home *** CD-ROM | disk | FTP | other *** search
Text File | 1994-09-04 | 196.2 KB | 4,258 lines |
- CHAPTER 12
-
- ASSEMBLY LANGUAGE PROGRAMMING
-
-
- This book has consistently presented programming techniques that reduce the
- size of your programs, and make them run faster. Most of the discussions
- focused on ways to write efficient BASIC code, and several showed how to
- access system interrupt services. Where speed was critical or BASIC was
- inflexible, I presented subroutines written in assembly language.
- Assembly language is the most powerful way to communicate with a PC, and
- it offers speed and flexibility unmatched by any other language. Indeed,
- assembly language is in many ways the ultimate programming language because
- it lets you control fully every aspect of your PC's operation. Anything
- that a PC is capable of doing can be accomplished using assembly language.
- This final chapter explains assembly language in terms that most BASIC
- programmers can understand.
- Why, you might ask, would a BASIC programmer be interested in assembly
- language? After all, the whole point of a high-level language such as
- BASIC is to shield the programmer from the underlying hardware. Without
- having to worry about CPU registers and memory addresses, a BASIC
- programmer can be immediately productive, and probably write programs with
- fewer initial bugs. However, there are three important reasons for using
- assembly language:
-
- ■ To speed up selected portions of a program
-
- ■ To reduce the size of a program
-
- ■ To perform services that BASIC simply cannot
-
- It is important to understand that any high-level language will benefit
- from the appropriate use of assembler. And while it is possible to write
- a major application using only assembly language, the increased complexity
- and added time to develop and debug it are often not worth the trouble.
- Using a high-level language--especially BASIC--for the majority of a
- program and then coding the size- and speed-critical portions in assembly
- language often is the most practical solution.
- Many BASIC programmers mistakenly believe that to achieve the fastest
- and smallest programs they should learn C. In my opinion, nothing could
- be further from the truth. Assembly language is barely more difficult to
- use than C, and in fact the code is often more readable. Further, no
- high-level language can come even close to what raw 8086 code can achieve.
- If you truly desire to become an advanced programmer, you owe it to
- yourself to at least see what assembly language is all about. I believe
- there is no deeper satisfaction than that gained by understanding fully
- what your computer is doing at the lowest level.
- This chapter assumes that you already understand basic programming
- concepts such as variables, arrays, and subroutines. As we proceed, most
- of the examples will provide parallels to BASIC where possible. But please
- remember one important point: There is nothing inherently difficult about
- assembly language. Attitude is everything, and if you can think of
- assembler as a stripped-down version of BASIC, you will be successful that
- much sooner.
- For ease of reading, I will refer to the 8088 microprocessor used in the
- IBM PC throughout this chapter. However, everything said about the 8088
- also applies to the 8086, the 80286, the 80386/486, and the NEC V series
- found in some older PC compatible computers. I will also use the terms
- assembly language and assembler interchangeably, although assembler can
- also be used to mean the program that assembles your source files.
- All of the examples in this chapter are meant to be assembled with the
- Microsoft Macro Assembler (MASM) version 5.1 or later. MASM requires that
- you save your source files as standard ASCII text, and most word processor
- programs can do this.
- Some of the examples in this chapter are derived from those that used
- CALL Interrupt in Chapter 11. In most cases I have not bothered to restate
- the same information from that chapter, and you may want to refer back for
- additional information.
- Finally, many entire books have been written about assembly language,
- and there is no way I can possibly teach you everything you need to know
- here. Rather, my intent is to provide a gentle introduction to the
- concepts using practical and useful examples.
-
-
- AS EASY AS BASIC
- ================
-
- Assembly language uses the same general form as a BASIC program. That is,
- commands are performed in sequence until a GOTO or GOSUB is encountered.
- In assembly language these are called Jump and Call, respectively. Many
- BASIC instructions have a direct assembler equivalent, although the syntax
- is slightly different. One important difference, however, is that the 8088
- microprocessor can operate on integer numbers only. Another is that for
- the most efficiency, you are limited to only a few working variables. I
- will begin by showing some rudimentary assembly language instructions, so
- you can see how they are analogous to similar commands in BASIC. Consider
- the following BASIC program fragment:
-
- AX = 5
-
- Here, the value 5 is assigned to the variable AX. The 8088 has several
- built-in variables called *registers*, and one of them is called AX. To
- move the value 5 into the AX register you use the Mov instruction:
-
- Mov AX,5
-
- As with BASIC, the destination variable in an assembly language program
- is always shown on the left, and the source is on the right. Now consider
- addition and subtraction. To add the value 12 to AX in BASIC you do this:
-
- AX = AX + 12
-
- The equivalent 8088 command is:
-
- Add AX,12
-
- Again, the variable or register on the left is always the one that receives
- the results of any adding, moving, and so on. Subtraction is very similar
- to addition, replacing Add with Sub:
-
- BASIC: AX = AX - 100
- Assembler: Sub AX,100
-
- Comparing and branching in assembly language is also quite similar to
- BASIC. But instead of this:
-
- AX = AX + 2
- IF AX > 60 GOTO Finished
-
- You'd do it in assembler this way:
-
- Add AX,2
- Cmp AX,60
- Ja Finished
-
- This tells the 8088 to add 2 to AX, then compare AX to 60, and finally to
- *jump if above* to the code at label Finished. There are several kinds of
- conditional jump instructions in assembly language, and they often follow
- a comparison as shown here. In fact, all you can really do after a compare
- is jump somewhere based on the results. And while there is no direct
- equivalent for this BASIC statement:
-
- IF AX = 10 THEN BX = BX - 1
-
- You can change the strategy to this:
-
- IF AX <> 10 GOTO Not10
- BX = BX - 1
- Not10:
- .
- .
-
- Now a direct translation is simple:
-
- Cmp AX,10
- Jne Not10
- Dec BX
- Not10:
- .
- .
-
- Jne stands for *Jump if Not Equal*. Also, notice the command Dec, which
- means decrement by 1. This is one case in which an assembler instruction
- is actually more to the point than its BASIC counterpart, and is equivalent
- to the BASIC command BX = BX - 1. While Sub BX, 1 would work just as well,
- using Dec is faster and generates less code, and we all know that speed is
- the name of the game.
- The complement to Dec is Inc, short for *increment by one*. You can use
- Inc and Dec with most of the 8088's registers, as well as on the contents
- of any memory location, which brings up an important issue. At some point,
- many programs will require more variables than can be held within the CPU's
- registers. All of the available free memory in a PC can be used as
- variable storage, with only a few limitations:
-
- ■ You must first tell the assembler how much space to set aside, much
- like you would when dimensioning an array. Moreover, MASM is pretty
- friendly and lets you use names for the memory locations. In fact, in
- most cases you do not need to know the memory addresses variables will
- be stored in--the assembler handles that for you as well!
-
- ■ Adding, subtracting, incrementing, and decrementing are all much
- faster when done within registers. When an operation is performed on
- a memory variable, it must first be fetched by the CPU, manipulated, and
- then stored again. Because the registers are within the CPU chip, those
- extra steps are not needed. The steps to retrieve and then store memory
- variables is handled transparently by the 8088; I mention this merely
- to explain why register operations are faster.
-
- ■ Some operations can be done only using registers. If you want to
- multiply the memory variable Counter by 12, you first have to move the
- variable into AX, do the multiplication, and then move it back into
- memory again. And if AX is currently holding a needed value, it must
- be saved before multiplying and restored again afterward. Although
- assembly language is not as complicated as many people think, it surely
- can be tedious at times.
-
- Besides the CPU registers and conventional memory addresses, a special
- portion of memory called the *stack* is also available for storage. The
- stack is much like the temporary memory on a four-function calculator, and
- it is often used to store intermediate results. The stack is also commonly
- used to pass variables between programs, because all programs can access
- it without having to know exactly where in memory it is located. Again,
- assembly language doesn't usually require you to deal with absolute memory
- addresses at all--especially for subroutines that will be added to a BASIC
- program. The only exceptions might be when writing directly to the display
- screen, or when looking at low memory, perhaps to see whether the Caps Lock
- key is engaged.
-
-
- SPAGHETTI CODE?
-
- To write a routine that converts lower case letters to capital letters in
- BASIC, you might use something like this:
-
- IF AL$ => "a" AND AL$ <= "z" THEN
- AL$ = CHR$(ASC(AL$) - 32)
- END IF
-
- In assembly language each compare must be done separately, followed by a
- jump based on the results. Let's rephrase the BASIC example slightly:
-
- IF AL$ < "a" GOTO Done
- IF AL$ > "z" GOTO Done
- AL$ = CHR$(ASC(AL$) - 32)
- Done:
- .
- .
-
- Now a conversion to assembler is easy:
-
- Cmp AL,"a" ;compare AL to "a"
- Jb Done ;Jump if Below to Done
- Cmp AL,"z" ;compare AL to "z"
- Ja Done ;Jump if Above to Done
- Sub AL,32 ;subtract 32 from AL
- Done:
- .
- .
-
- Notice how the assembler allows the use of quoted constants. When it sees
- a character or string in double or single quotes, it knows you mean to use
- the character's ASCII value. Unlike BASIC with its strong variable typing
- that prevents you from performing numeric operations on a string, assembly
- language has very few such restrictions. Also notice how much jumping
- around is necessary to accomplish even the simplest of actions.
- As I mentioned earlier, assembly language can certainly be more tedious
- than BASIC, although the logic is not really that different. Such frequent
- jumping around is called spaghetti code by some programmers, and it is
- often used in a derogatory fashion when discussing BASIC's GOTO statement.
- But this is the way that computers work, and I am amused by programmers who
- argue so strongly against all use of the GOTO command. While nobody could
- seriously object to a well organized and structured programming style, all
- programs are eventually converted to equivalent assembly language jumps and
- branches.
-
-
- THE REGISTERS
- =============
-
- There are six general purpose registers available for you to use: AX, BX,
- CX, DX, SI, and DI. Each register may be used for the most common
- operations like adding and subtracting, although some are specialized for
- certain other operations. However, most of the registers also have a
- specialty. For example, AX is the only register that can be multiplied or
- divided. The A in AX stands for Accumulator, and it often used for math
- operations such as accumulating a running total. Also, several assembler
- instructions result in one byte less code when used with AX, when compared
- to the same instructions using other registers.
- The B in BX means Base, and this register is frequently used to hold the
- base address of a collection of variables or other data. If you have a
- text string in memory to be examined, you could put the address of the
- first character in BX. The rest of the string can then be found by
- referencing BX.
- BX can also be used to specify computed addresses using addition or
- subtraction. For example, the instruction Mov AX,[BX+4] means to load AX
- with the word four bytes beyond the address held in BX. Likewise, the
- instruction Add DL,[BX+SI-10] adds the value of the byte at that computed
- address to the current contents of DL. You may use BX this way with either
- a constant number, the SI or DI register, or one of those registers and a
- constant number. However, only addition and substraction may be used, as
- opposed to multiplication or division. I will return to computed and
- indirect addressing later in this chapter.
- The C in CX stands for Count, since CX is most often used as the counter
- in an assembly language FOR/NEXT loop. In fact, the assembly language
- command Loop uses CX to perform an operation a specified number of times.
- The comparison below illustrates this.
-
- BASIC:
- FOR CX = 1 TO 5
- GOSUB BeepTone
- NEXT
-
- Assembler:
- Mov CX,5
- Do: Call Beep_Tone
- Loop Do
-
- Here, the Loop instruction automatically branches to the label Do: CX
- times. That is much faster and more efficient than this:
-
- Mov CX,5
- Do: Call Beep_Tone
- Dec CX
- Cmp CX,0
- Jne Do
-
- The DX register is a general purpose Data register, and is named
- accordingly. DX is also used in conjunction with AX when multiplying and
- dividing.
- The last two general purpose registers are SI and DI. SI stands for
- Source Index, while DI means Destination Index. It is not hard to guess
- that these registers are well suited for copying data from one memory
- location to another. The 8088 has a rich set of instructions for moving
- and comparing strings, using SI and DI to show where they are.
- Like BX, SI and DI may be used with a constant offset such as [SI+100]
- to compute a memory address, or with a constant value and/or BX. But
- again, SI and DI are still general purpose registers, and they can be used
- for common chores as well. In many situations it really doesn't matter
- whether you use BX or DI or SI or AX.
- There are two specialized registers called BP and SP. BP (Base Pointer)
- is another Base register like BX, only it is intended for use with the
- stack. When you need to access data on the stack, BP is the most
- appropriate register to use. Like BX, BP can reference computed addresses
- with a constant offset, with SI or DI, or with a constant and SI or DI.
- The SP (Stack Pointer) register holds the current address of the stack,
- and it should never be altered unless you have a very good reason to do so.
- The last four registers are the segment registers, but I will mention
- them only briefly right now. As you undoubtedly know, the 8088 used a
- segmented architecture; although it can utilize a megabyte of memory, it
- can do so only in 64K portions at a time. The CS register holds the
- current Code Segment (your program code), DS holds the Data Segment (your
- memory variables), SS holds the Stack Segment, and ES is an Extra Segment
- that is often used to access arrays located in far memory.
- Each of the 8088 registers can hold one word (two bytes), allowing you
- to store any integer number between 0 and 65535. This range of values can
- also be considered as -32768 to 32767. But AX, BX, CX, and DX may also be
- used as two separate one-byte registers with a range of either 0 to 255 or
- -128 to 127. One byte is often sufficient--for example, when manipulating
- ASCII characters--and this ability to access each half individually
- effectively adds four more registers. Remember, the more variables you can
- keep within registers, the faster and more efficient a program will be.
- When using the registers separately, the two halves are identified by
- the letters H and L, for High and Low. That is, the high portion of AX is
- referred to as AH, while the low portion of DX is called DL. This would
- be represented with BASIC variables as follows:
-
- AX = AL + 256 * AH
-
- Each half can also be represented as bit patterns:
-
- AX
- ┌──────────────────────┐
- 1011 0110 0111 0101
- └──────────┘└──────────┘
- AH AL
-
- Notice that SI, DI, BP, and SP cannot be split this way, nor can the
- segment registers CS, DS, SS, and ES.
- There is also another register called the Flags register, though it is
- not intended for you to use directly. After performing calculations and
- comparisons, certain bits in the Flags register are set or cleared by the
- CPU automatically, depending on the results. For example, if you add a
- register that holds the value 40000 to another register whose value is
- 30000, the Carry flag will be set to show that the result exceeded 64K.
- The 8088 flags are also set or cleared to reflect the result of a Cmp
- (Compare) instruction. Although you will not usually access these flags
- directly, they are used internally to process Jne, Ja, and the other
- conditional jump commands.
-
-
- VARIABLES IN ASSEMBLY LANGUAGE
- ==============================
-
- All of the example routines shown so far have used the 8088 registers as
- working variables. Indeed, using registers whenever possible is always
- desirable because they can be accessed very quickly. But in many real-
- world applications, more variables are needed than can fit into the few
- available registers. As with BASIC, MASM lets you define variables using
- names you choose, and you must also specify the size of each variable.
- The first step is to define the amount of space that will be set aside
- with the assembler instructions DB and DW. These stand for Define Byte and
- Define Word respectively, and they allocate either one byte of storage or
- two. You can also use DD to define a double word long integer variable.
- Notice that these are not commands that the 8088 processor will execute;
- rather, they inform the assembler to leave room for the data. Some
- examples are shown below:
-
- MyByte DB 12h ;one byte, preset to 12h
- Buffer DB 15 Dup(0) ;fifteen bytes, all 0
- Dummy DW ? ;one word (two bytes), 0
- Msg DB "Test message",13,10 ;message, CR, LF
-
- In the first example one byte of memory is allocated using the name MyByte,
- and the value 12 Hex is placed there at assembly time. The second example
- illustrates using the Dup (duplicate) command, and tells MASM to set aside
- fifteen bytes filling each with the specified value. In this case that
- value is zero. Initialized data is an important feature of assembly
- language, and one that is sorely missing from BASIC. By being able to
- allocate data values at assembly time, additional code to assign those
- values at runtime is not needed.
- Filling an area with zeroes can also be accomplished with a question
- mark, and this is frequently used when the value that will eventually end
- up there is not known in advance. Both do the same thing in most cases,
- however using "?" implies an unknown, as opposed to an explicit zero. You
- may use whichever method seems more appropriate at the time. The last
- example shows how text may be specified, as well as combining values in a
- single statement.
- Since the assembler lets you use names for your data, fetching or
- storing values can be done with the normal Mov instruction like this.
-
- Error_Code DB ?
- Mov Error_Code,AL
-
- This puts the contents of register AL into memory location Error_Code.
- Getting it back again later is just as easy:
-
- Mov DH,Error_Code
-
- Sometimes the assembler needs a little help when you assign variables.
- When you move AL or DH in and out of a memory location, the assembler knows
- that you are dealing with a single byte. And if you specify BX or SI as
- the source or destination operand, the assembler understands this to mean
- two bytes, or one word. But when literal numbers are used, the size of the
- value is not always obvious. Consider the following:
-
- Mov [BX],3Ch
-
- Does this mean that you want to put the value 3Ch into the byte at the
- address held in BX, or the value 003Ch into the *word* at that address?
- There is no way for MASM to know what your intentions are, so you must
- specify the size explicitly. This is done with the Byte Ptr and Word Ptr
- directives. Here, Ptr stands for Pointer, and two examples are shown:
- Mov Byte Ptr [BX],15
- Mov Word Ptr ES:[DI],100
-
- The first example specifies that the memory at address BX is to be treated
- as a single byte. Had Word been used instead, a 15 would be placed into
- the byte at address held in BX, and a zero would be put into the byte
- immediately following. Words are always stored with the low-byte before
- the high-byte in memory.
- Memory variables are accessed using the normal complement of
- instructions. For example, to add 15 to the variable Counter you will use
- Add Counter,15. And to multiply AX by the word variable Number you will
- use Mul Word Ptr Number. In MASM versions 5.0 and later, the Word Ptr
- argument is not strictly necessary. That is, if Number had been defined
- using DW, then MASM knows that you mean to multiply by a word rather than
- a byte. But earlier versions of the assembler were not so smart, and an
- explicit Word Ptr or Byte Ptr was required.
- Note, however, that you must still use Byte Ptr or Word Ptr to override
- a variable's type. For example, if Value was defined as a word but you
- want to access just its lower byte, you must use Mov AL,Byte Ptr Value.
- Here, stating Byte Ptr explicitly tells MASM that you are intentionally
- treating Value as a different data type. Otherwise, it will issue a non-
- fatal warning error message.
- Sometimes you may want to refer to the address of a variable, as opposed
- to its contents. For example, Mov AX,Variable tells MASM to move the value
- held in Variable into the AX register. But many DOS services require that
- you specify a variable's address in a register. This is done using the
- Offset operator: Mov DX,Offset Buffer. Where Mov DX,Buffer places the
- first two bytes of the buffer into DX, using Offset tells MASM that you
- instead want the starting address of the buffer.
- You can also use the Lea (Load Effective Address) command to obtain an
- address, but that is less frequently used. Although Lea DX,Buffer can be
- used to load DX with the starting address of Buffer, it is a slightly
- slower instruction. Lea is needed only when an address must be computed.
- For example, the instruction Lea SI,[BX+DI] loads SI with the sum of the
- BX and DI registers. You may notice that Lea can provide a shortcut for
- adding or subtracting certain register combinations. Although this use of
- Lea is uncommon, Lea can replace the following two instructions:
-
- Mov SI,BX
- Add SI,DI
-
- To subtract two registers or a register and a constant value you could use
- Lea AX,[BX-DI] or Lea SI,[BP-10].
-
-
- CALCULATIONS IN ASSEMBLY LANGUAGE
- =================================
-
- When adding or subtracting you may use two registers, or a register and a
- memory variable. It is not legal to specify two memory variables as in
- Add Var1,Var2.
- Multiplying and dividing are not so flexible; only AL and AX may be
- multiplied. When dividing, the numerator must be either in AX, or the long
- integer comprised of DX:AX. In this case, DX holds the upper word and AX
- holds the lower one. However, you may multiply or divide these registers
- using either a register or a memory location. Because of this restriction,
- it is not necessary to specify the target operand size. That is, Mul CL
- means to multiply AL by CL leaving the result in AX, and Div WordVariable
- divides DX:AX by the contents of WordVariable leaving the result in AX and
- the remainder in DX. Although you could use the commands Mul AL,CL and Div
- AX,WordVariable, this is not necessary or common.
- All of the allowable combinations for multiplying and dividing are shown
- in Figure 12-1.
-
-
- Instruction Operand Result Remainder
- ════════════════ ═══════ ══════ ═════════
- Mul ByteRegister AL AX n/a
- Mul ByteVariable AL AX n/a
- Mul WordRegister AX DX:AX n/a
- Mul WordVariable AX DX:AX n/a
-
- Div ByteRegister AX AL AH
- Div ByteVariable AX AL AH
- Div WordRegister DX:AX AX DX
- Div WordVariable DX:AX AX DX
-
- Figure 12-1: The allowable register/memory combinations for multiplying and
- dividing.
-
-
- In Figure 12-1 ByteRegister means any byte-sized register such as AL or
- CH; WordRegister indicates any word-sized register like CX or BP.
- Likewise, ByteVariable and WordVariable specify byte- and word-sized
- integer memory variables respectively.
- It's important to understand that you must never divide by zero, because
- that will generate a critical error. Because the result from dividing by
- zero is infinity, the 8088 has no way to handle that--it can't simply
- ignore the error. Therefore, dividing by zero causes the CPU to generate
- an Interrupt 0. In a BASIC program that error is routed to BASIC's
- internal error handling mechanism which either invokes the ON ERROR handler
- if one is in effect, or ends your program with an error message. In a
- purely assembly language program, DOS intervenes printing an error message
- on the screen, and then it ends the program.
- Related to division by zero is dividing when the result cannot fit into
- the destination register. For example, if AX holds the value 20000 and you
- divide it by 2, the resulting 10000 cannot fit into AL. Since this is
- another unrecoverable error that cannot be ignored, the 8088 generates an
- Interrupt 0 there as well.
- Besides the Div and Mul instructions, there are also signed versions
- called Idiv and Imul. Where Div and Mul treat the contents of AX or DX:AX
- as an unsigned value, Idiv and Imul treat them as being signed. You'll use
- whichever command is appropriate, so the 8088 knows if values having their
- highest bit set are to be treated as negative. BASIC always uses Idiv and
- Imul in the code it generates, since all integer and long integer values
- are treated by BASIC as signed.
- Because only AX and DX:AX may be used for multiplying and dividing, this
- affects your choice of registers. The short example that follows shows how
- you might select registers when translating a simple BASIC-like expression
- that uses only integer (not long integer) variables.
-
-
- BASIC:
- Result = (Var1 + Var2 * (Var3 - Var4)) \ 100
-
-
- Assembler:
- Mov AX,Var3 ;work from the innermost level out
- Sub AX,Var4 ;so first perform Var3 - Var4
- Imul Word Ptr Var2 ;then multiply that by Var2
- Add AX,Var1 ;add Var1 to what we have so far
- Mov DX,0 ;next prepare to divide DX:AX
- Mov CX,100 ;use CX for the divisor
- Idiv CX ;do the division
- Mov Result,AX ;then assign Result ignoring the
- ; remainder left in DX
-
-
- Because dividing by an integer value uses both DX and AX, it is necessary
- to clear DX explicitly as shown unless you are certain it is already zero.
- The use of CX to hold the value 100 is arbitrary. If CX were currently in
- use, any available word-sized register or memory location could be used.
- If you compile this program statement and view the resultant code using
- CodeView, you will see that BASIC does an even better job of translating
- this particular expression to assembly language.
-
-
- STRING PROCESSING INSTRUCTIONS
- ==============================
-
- Besides being able to add, subtract, multiply, and divide, the 8088
- provides four very efficient instructions for manipulating strings and
- other data in memory. Movs copies, or moves a string from place to
- another; Cmps compares two ranges of memory; Stos fills, or stores one or
- more addresses with the same value; and Scas scans a range of memory
- looking for a particular value. These instructions require either a byte
- or word specifier. For example, you would use Movsb to copy a byte, and
- Cmpsw to compare two words.
- There are two important factors that contribute to the power and
- usefulness of these string instructions: each is only one byte long, and
- they automatically increment or decrement the SI and DI registers that
- point to the data being manipulated. Thus, they are both convenient to
- use, and also very fast. Because it is common to access blocks of memory
- sequentially a byte or word at a time, automatically advancing SI and DI
- saves you from having to do that manually with additional instructions.
- For example, after one pair of words has been compared, SI and DI are
- already set to point at the next pair.
- You can also specify that SI and DI are to be decremented by first using
- the Std (Set Direction) command. The Direction Flag stores the current
- string operations direction, which is either up or down. If a previous Std
- was in effect, then you'd use Cld (Clear Direction) to force copying and
- moving to be forward. In fact, BASIC *requires* you to clear the direction
- flag to forward before returning from a routine that set it to backwards.
-
-
- MOVS AND CMPS
-
- Movs and Cmps use the DS:SI register pair to point to the first range of
- memory being copied or compared, and ES:DI to point to the second range.
- Each time a byte is being copied or compared, SI and DI are incremented or
- decremented by one to point to the next address. And when a word is being
- accessed, SI and DI are incremented or decremented by two.
- Notice that there is no protection against SI or DI being incremented
- or decremented through address zero, nor is there any indication that this
- has happened. Also notice that the name Movs is somewhat of a misnomer.
- To me, moving something implies that it is no longer at its original
- location. Movs does not alter the source data at all--it merely places a
- new copy at the specified destination address.
-
-
- SCAS AND STOS
-
- Scas compares the value in AL or AX with the range of memory pointed to
- by ES:DI. That is, Scasb compares AL and Scasw uses AX. Stos also uses
- ES:DI to show where the data being written to is located; Stosb stores the
- contents of AL in the address at ES:[DI] and then increments or decrements
- DI by one. Likewise, Stosw stores the value in AX there and increments or
- decrements DI by two.
-
-
- REPEATING STRING OPERATIONS
-
- If these four instructions merely acted on the data and incremented SI and
- DI automatically, that would be very useful indeed. But they also have
- another talent: they recognize a Rep (Repeat) prefix to perform their magic
- a specified number of times. The number of iterations is specified by the
- count held in CX. Furthermore, the number of repetitions can be made
- conditional when comparing and scanning, based on the data encountered.
- If you have, say, 20 bytes of data that need to be copied from one place
- to another, you would first set CX to 20 and then use Rep Movsb. And to
- compare 100 words you would load CX with the value 100 and use Rep Cmpsw.
- Stos also accepts a Rep prefix; Rep Stosb places the value in AL into CX
- bytes of contiguous memory starting at the address specified in ES:DI. For
- each iteration the 8088 decrements CX, and when it reaches zero the copying
- or comparing is complete.
- It is usually not valuable to scan a range of memory unconditionally and
- repeatedly. Therefore Scas is generally used in conjunction with either
- Repe (Repeat while Equal) or Repne (Repeat while Not Equal). Cmps is also
- generally used with these conditional prefixes, to avoid wasting time
- comparing bytes after a match or a difference was found. In either case,
- however, you load CX with the total number of bytes or words being compared
- or scanned.
- Because each iteration decrements CX, you can easily calculate how many
- bytes or words were actually processed. Also, you can test the results of
- scanning and comparing using the normal methods such as Je and Jne. The
- following few examples show some ways these commands can be used.
-
- See if two 40-byte ranges of memory are the same:
-
- Mov CX,20 ;comparing 20 words is faster than 40 bytes
- Repe Cmpsb ;compare them
- Je Match ;they matched
-
- Copy a 2000-element integer array to color screen memory:
-
- Mov AX,ArraySeg ;set DS to the source segment
- Mov DS,AX ;through AX
- Mov SI,ArrayAdr ;point SI to the array start
- Mov AX,&HB800 ;the color text screen segment
- Mov ES,AX ;assign that to ES
- Mov DI,0 ;clear DI to point to address 0
- Mov CX,2000 ;prepare to copy 2000 words
- Rep Movsw ;copy the data
-
- Search a DOS string looking for a terminating zero byte:
-
- Mov AX,StringSeg ;set ES to the string's segment
- Mov ES,AX ;(ES cannot be assigned directly)
- Mov DI,Offset ZString ;point DI to the string data
- Mov CX,80 ;search up to 80 bytes
- Mov AL,0 ;looking for a zero value
- Repne Scasb ;while ES:[DI] <> AL
- ;-- Now DI points just past the terminating zero byte.
- ;-- The length of the string is (80 - CX + 1).
-
- In the first example, it is assumed that DS:SI and ES:DI already point to
- the correct segment and address. By asking to compare only while the bytes
- are equal, the result of the most recent byte comparison can be tested
- using Je. A common mistake many programmers make is comparing the bytes,
- and then checking if CX is zero. The reasoning is that if CX is zero then
- they must have all matched; otherwise, the 8088 would have aborted the
- comparisons early. But CX will also be zero if all but the last byte
- matched! Therefore, you must check the zero flag using Je (or Jne if that
- is more appropriate).
- Notice in the first example how 20 words are compared, rather than 40
- bytes. Although the net result is the same, word operations are faster on
- 80286 and later processors when the blocks of memory begin at an even
- numbered address. [Though you can't always know if a variable or block
- of memory will begin at an even address, using the word version will be
- more efficient at least some of the time.]
- The second and third examples include the code needed to set up the
- appropriate segment and address values in DS:SI and ES:DI. Although this
- may seem like a lot of work, you can often do this setup only once and then
- use the same registers repeatedly within a routine. Unfortunately, you
- are not allowed to assign a segment register from a constant number. You
- must first assign the number to a conventional register, and then use Mov
- to copy it to the segment register.
-
-
- THE STACK
-
- The primary purpose of the stack is to retain the return address of a
- program when a subroutine is called. This is true not only for assembly
- language, but for BASIC as well. For example, when you use the BASIC
- statement GOSUB 1200, BASIC must remember the location in memory of the
- next command to execute when the routine returns. It does this by placing
- the address of the next instruction onto the stack *before* it jumps to the
- subroutine. Then when a RETURN instruction is encountered, the address to
- return to is available. The 8088 understands Calls and Returns directly,
- and it places and restores the addresses on the stack automatically.
- The stack is not unlike a stack of books on a table, and one of its
- great advantages is that you don't need to know where in memory it is
- actually located. Items can be placed onto the stack either manually with
- the Push instruction, or automatically by the 8088 processor as part of its
- handling of Call and Return statements. Values are retrieved from the
- stack with the Pop command, among other methods.
- One important feature of the stack is when items are added and removed,
- the stack pointer register is updated automatically to reflect the next
- available stack location. Thus, a program can access items on the stack
- based on the stack pointer, rather than have to know the exact address at
- any given time. This simplifies exchanging information between programs,
- since neither has to know how the other operates. This mechanism also
- makes it possible for programs written in one language to communicate with
- subroutines written in another.
- Figure 12-2 shows how the stack operates.
-
-
- │
- │
- │
- │
- ├──────────┤
- │ Item 1 │ <── first item that was pushed
- ├──────────┤
- │ Item 2 │ <── second item that was pushed
- ├──────────┤
- │ Item 3 │ <── third item that was pushed
- ├──────────┤
- │ Item 4 │ <── last item that was pushed (SP points here)
- ├──────────┤
- │ Next │ <── next available stack location
- ├──────────┤
- │ ┌── the stack grows downward
- │ │ as new items are added
- │ │
- │ │
- \/
-
- Figure 12-2: The organization of the CPU stack.
-
-
- As each item is pushed onto the stack, it is placed two bytes below the
- address held in the stack pointer. Then the stack pointer is decremented
- by two, to show the next available stack location. Therefore, the stack
- grows downward as new items are added. Note that only full words may be
- pushed onto the stack, so all of the items shown here are two bytes in
- size. Also note that the stack pointer holds the address of the last item
- that was pushed.
-
-
- PASSING PARAMETERS
- ==================
-
- Imagine you have a BASIC subroutine that does something to the variable X.
- The code to assign X, process, and print X might look like this:
-
- X = 12
- GOSUB 2000 'the routine at line 2000 manipulates X
- PRINT X
-
- In assembly language you could push the value 12 onto the stack, and then
- call the subroutine. The subroutine, expecting the value there would
- retrieve it, do its work, and then place the result back again before
- returning. This is similar, but not identical, to how variables are passed
- between programs. Most high-level languages including BASIC pass variables
- to subroutines by placing their *addresses* on the stack. A called routine
- can then access the variable via its address, either to read it or to
- assign a new value.
- If BASIC let you access the registers directly, it could pass variables
- through them, as you saw when telling DOS which of its services to do. But
- BASIC doesn't allow that and moreover, with a limited number of registers,
- only a few variables or addresses could be accommodated. The stack can
- hold any number of arguments, by pushing the address of each in turn.
- When you use the BASIC CALL command and pass a variable name to a SUB
- or FUNCTION procedure, BASIC first pushes the address of that variable onto
- the stack, before jumping to the code being called. And if more than one
- variable is specified, all of the addresses are pushed. The example below
- shows how you might call a routine that returns the current default drive.
-
- CALL GetDrive(Drive%)
-
- When GetDrive begins, it knows that the stack is holding the address of
- Drive%. The segment and address of the calling BASIC program is also on
- the stack; however, GetDrive is not concerned with that. The important
- point is that it can find the address on the stack using the SP (Stack
- Pointer) register. When GetDrive begins the stack is set up as shown in
- Figure 12-3.
-
-
- │ ^
- │ │
- │ │
- │ └── higher addresses
- ├──────────┤
- │ Drive% │ <── the address of Drive% that BASIC pushed
- ├──────────┤
- │ Ret Seg │ <── BASIC's segment to return to
- ├──────────┤
- │ Ret Adr │ <── BASIC's address to return to (SP holds this address)
- ├──────────┤
- │ Next │ <── the next available stack location
- ├──────────┤
- │
- │
- │
- │
-
- Figure 12-3: The state of the stack within a procedure when one variable
- address was passed.
-
-
- Notice that while GetDrive can get at the address of Drive% through SP,
- an extra step is still required to get at the *data* held in Drive%. Let's
- digress for a moment to reconsider the difference between memory addresses
- and values. The assembler command Mov AX,12 puts the value 12 into
- register AX. But suppose you want to put the contents of *memory location*
- 12 into AX. You indicate this to the assembler by using brackets, as shown
- in the two equivalent examples following.
-
- Mov AX,[12] ;load AX from address 12
-
- Mov BX,12 ;assign BX to the value 12
- Mov AX,[BX] ;load AX from the address held in BX
-
- The first statement loads AX from the contents of memory at address 12.
- The second first loads BX with the number 12, and then uses BX to identify
- that address, moving the contents of that address into AX. This is an
- important distinction, and is illustrated in Figure 12-4 using parallels
- to BASIC's PEEK and POKE commands.
-
-
- BASIC Assembler
- ════════════════════ ═════════════════════
- BP = SP Mov BP,SP
- AL = PEEK(BP + 8) Mov AL,[BP+8]
- SI = 12 Mov SI,12
- POKE SI, 12 Mov Byte Ptr [SI],12
-
- Figure 12-4: Similarities between BASIC's PEEK and POKE, and the assembly
- language Mov instruction.
-
-
- Although you can easily find the address of Drive% by looking at SP, an
- extra step is required to get at the actual value. The example that
- follows shows how to do this, except there is one added complication. You
- are not allowed to use SP for addressing, except with 386 and later
- microprocessors. Since you undoubtedly want your programs to work with as
- many computers as possible, a different strategy must be used.
- As I mentioned earlier, the BP register is a base register that is meant
- for accessing data on the stack. Therefore, you must first copy SP into
- BP, and then use BP to access the stack. Then you can find where Drive%
- is located, and put the current drive number into that address as shown
- following:
-
- Mov BP,SP ;put the current stack pointer into BP
- Mov SI,[BP+4] ;put the address of Drive% into SI
- Mov AH,19h ;tell DOS we want the default drive
- Int 21h ;call DOS to do it
- Mov [SI],AL ;put the answer into Drive%
-
- Notice how brackets are used to indicate the addresses. You must first
- determine the address of Drive%'s address (whew!), before you can put the
- value held in AL there. This is called indirect addressing, because a
- register is used to hold the address of the data. Again, notice how the
- 8088 accepts addition on the fly when you tell it BP+4.
- The complete working GetDrive routine has two small added complications.
- Beside being unable to use SP for addressing memory, BASIC also requires
- you to not change BP either. The obvious solution, therefore, is to first
- save BP on the stack before changing it, and then restore BP later before
- returning to BASIC. The other complication is caused by the very fact that
- BASIC put extra information (Drive%'s address) onto the stack. But neither
- is insurmountable, as shown here:
-
- Push BP ;save BP before changing it
- Mov BP,SP ;put the stack pointer into BP
- Mov SI,[BP+6] ;put the address of Drive% into SI
- Mov AH,19h ;tell DOS we want default drive
- Int 21h ;call DOS to do it
- Mov [SI],AL ;put the answer into Drive%
- Pop BP ;restore BP to its original value
- Ret 2 ;return to BASIC
-
- Notice that here, the address of Drive% is at [BP+6] rather than [BP+4]
- as it was in the previous listing. Since BP was pushed at the start of the
- procedure, the stack pointer is two bytes lower when it is subsequently
- assigned to BP. When SI is loaded, [BP] points to the saved version of
- itself, [BP+2] and [BP+4] point to the address and segment to return to,
- and [BP+6] holds the address of Drive%'s address. This is illustrated in
- Figure 12-5.
-
-
- │
- │
- │
- │
- ├──────────┤
- │ Drive% │ <── [BP+6] points here
- ├──────────┤
- │ Ret Seg │ <── [BP+4] points here
- ├──────────┤
- │ Ret Adr │ <── [BP+2] points here
- ├──────────┤
- │ Saved BP │ <── [BP] points here
- ├──────────┤
- │ Next │ <── the next available stack location
- ├──────────┤
- │
- │
- │
- │
-
- Figure 12-5: The state of the stack within a procedure after BP has been
- pushed.
-
-
- Normally when a Ret command is encountered, the 8088 pops the last four
- bytes from the stack automatically, and returns to the segment and address
- contained in those bytes. But that would leave the 2-byte address of
- Drive% still cluttering up the stack. To avoid this problem the 8088 lets
- you specify a *parameter count* as part of the Ret instruction.
- For each variable address that is passed with a CALL from BASIC, you
- must add 2 to the Return instruction in your assembler routine. This is
- the number of bytes to remove from the stack, with two being used for each
- incoming two-byte address. Had two variables been passed, the program
- would have used Ret 4 instead. Although it is possible to have the calling
- program clean up the stack itself, that would be wasteful.
- For every occurrence of every call that passes parameters, BASIC would
- have to include additional code following the call to increment SP
- accordingly. Pushing a parameter's address onto the stack leaves that much
- less stack space available. Therefore, someone has to reverse the process
- and either pop the addresses or use Add SP,Num to adjust the stack pointer.
- By having the called routine handle it, that code is needed only once. In
- fact, this is an important deficiency of C, because by design C requires
- the caller to clean up the stack.
- [If you've managed to persevere this far you'll be pleased to know that
- in practice, the assembler can be told to handle most or all aspects of
- stack addressing for you. This is discussed in the sections that follow.]
- It is also possible to tell BASIC to pass some types of parameters by
- value using the BYVAL option in the DECLARE or CALL statements. When BYVAL
- is used, BASIC places the actual value of the variable onto the stack,
- rather than its address. This has several important benefits. First, the
- assembly language routine can use one less instruction. Second, when a
- constant number is passed, BASIC does not need to make a copy of it in
- DGROUP. This copying was described in Chapter 2.
- However, BYVAL is appropriate only when a parameter does not have to be
- returned, and only when the values are integers. If you pass a double
- precision parameter using BYVAL, all eight bytes are placed on the stack
- using four separate instructions rather than only two needed to pass the
- address. You can also instruct BASIC to pass the full, segmented address
- of a parameter, and that is discussed in the section "Dynamic Arrays."
-
-
- PROCEDURES IN ASSEMBLY LANGUAGE
- ===============================
-
- All of the discussions so far have focused on how to write the instructions
- for an assembly language subroutine. However, none have described how
- these routines are added to a BASIC program, or how a complete procedure
- is defined. Furthermore, the previous examples have not shown a key step
- that is needed with all such external routines: establishing the code and
- data segments.
- Before an external routine can be linked to a BASIC program you must
- establish a public procedure name that LINK can identify. I will first
- show the formal method for defining a procedure and its segments, and then
- show the newer, simplified methods that were introduced with MASM version
- 5.1. The simplified syntax is used for all of the remaining examples in
- this chapter [so don't worry if the setup details for this first example
- appear overwhelming].
- The simplest complete subprogram you are likely to encounter is probably
- the PrtSc routine that follows--all it does is call Interrupt 5 to send the
- contents of the current display screen to LPT1.
-
-
- Code Segment Word Public 'Code'
- Assume CS:Code
- Public PrtSc
- PrtSc Proc Far ;this is equivalent to SUB PrtSc STATIC in BASIC
-
- Int 5 ;call BIOS interrupt 5
- Ret ;return to BASIC
-
- PrtSc Endp ;this is equivalent to BASIC's END SUB
- Code Ends
- End
-
-
- The first three lines tell the assembler that the code is to be placed in
- the segment named Code, and that the name PrtSc is to be made public. The
- fourth line defines the start of a procedure. The actual code occupies
- the next two lines. Of course, you must tell the assembler where the
- procedure ends, which in this case is also the end of the code segment.
- Had several procedures been included within the same block of code, each
- procedure would show a start and end point, but there would only be a
- single code segment. The final End statement is needed to tell the
- assembler that this is the end of listing, although you might think that
- MASM would be smart enough to figure that out by itself!
- Notice that there are two kinds of procedures: Far and Near. External
- routines that are called from BASIC are always Far, because BASIC uses what
- is called a *medium model*. This means the procedure does not necessarily
- have to be within the same code segment as the main BASIC program. The
- medium model allows the combined programs to exceed the usual 64k limit
- when linked to a final .EXE file.
- When BASIC executes a CALL command, it uses a two-word address as the
- location to jump to. One of the words contains a segment, and the other
- an address within that segment. Then when your program finally returns,
- the 8088 must know to remove two words from the stack--a segment and an
- address--to find where to return to in the calling BASIC program.
- A near procedure, on the other hand, calls an address that is only one
- word long. And when the procedure returns, only a single word is popped
- from the stack. Again, the assembler does the bulk of the dirty work for
- you. You just have to remember to use the word Far.
-
-
- SIMPLIFIED DIRECTIVES
-
- Fortunately, Microsoft realized what a pain dealing with segments and
- procedures and offsets from BP can be, and they enhanced MASM beginning
- with version 5.0 to handle these details automatically for you. Rather
- than require the programmer to define the various code and data segments,
- all that is needed are a few simple key words.
- The first is .Model Medium, which tells MASM that the procedures that
- follow will be Far. Used in conjunction with .Code and .Data, .Model
- Medium tells MASM that any data you define should be placed into a group
- named DGROUP. Adding ,Basic after the .Model directive also declares your
- procedures as Public automatically, so BASIC can access them when your
- program is linked.
- By using the name DGROUP, the linker automatically gathers all of your
- DB and DW data variables, and places them into the same segment that BASIC
- uses. While this has the disadvantage of impinging on BASIC's near data
- space, it also means that on entry to the routine the DS register (which
- BASIC sets to hold the DGROUP segment) hold the correct segment value for
- your variables as well.
- To show the advantages of simplified directives, contrast the earlier
- PrtSc with this version that does exactly the same thing:
-
-
- .Model Medium, Basic
- .Code
-
- PrtSc Proc
- Int 5
- Ret
- Endp
- End
-
-
- MASM 5.1 introduced additional simplified directives that let you access
- incoming parameters by name, rather than as offsets from BP. All of the
- remaining examples in this chapter take advantage of simplified directives,
- as the following revised listing for GetDrive illustrates.
-
-
- ;Syntax: CALL GetDrive(Drive%)
-
- .Model Medium, Basic
- .Data
- ;-- if variables were needed they would be placed here
-
- .Code
- GetDrive Proc, Drive:Word
-
- Mov AH,19h ;tell DOS we want the default drive
- Int 21h ;call DOS to do it
- Mov BX,Drive ;put the address of Drive% into BX
- Cbw ;clear AH to make a full word
- Mov [BX],AL ;then store the answer into Drive%
- Ret ;return to BASIC
-
- GetDrive Endp ;indicate the end of the procedure
- End ;and the end of the source file
-
-
- As you can see, this looks remarkably like a BASIC SUB or FUNCTION
- procedure, with the incoming parameter listed by name and type as part of
- the procedure declaration. This greatly simplifies maintaining the code,
- especially if you add or remove parameters during development. If incoming
- parameters are defined as shown here using Drive%, code to push BP and then
- move SP into BP is added for you automatically. When you refer to one of
- the parameters, the assembler substitutes [BP+##] in the code it generates.
- Note, however, that the Word identifier for Drive refers to the 2-byte size
- of its address, and not the fact that Drive% is a 2-byte integer.
- Also notice the new Cbw command, which is used here to clear the AH
- register. Cbw (Convert Byte to Word) expands the byte value held in AL to
- a full word in AX. A full word is needed to ensure that both the high- and
- low-byte portions of Drive% are assigned, in case it held a previous value.
- If the value in AL is positive (between 0 and 127), AH is simply cleared
- to zero. And if AL is negative (between -128 and -1 or between 128 and
- 255), Cbw instead sets all of the bits in AH to be on. Thus, the sign of
- the original number in AL is preserved.
- A complementary statement, Cwd (Convert Word to Double Word), converts
- the word in AX to a double-word in DX:AX. Again, if AX is positive when
- considered as a signed number, DX is cleared to zero. And if AX is
- currently negative, DX is set to FFFFh (-1) to preserve the sign. Cbw and
- Cwd are both one-byte instructions, so even with unsigned values they are
- always smaller and faster for clearing AH or DX than Mov AH,0 and Mov DX,0
- which require two bytes and three bytes respectively.
- Finally, the Ret command that exits the procedure is translated by MASM
- to include the correct stack adjustment value, based on the number of
- incoming parameters. If you have multiple exit points from the procedure
- (equivalent to EXIT SUB), the exit code will be generated multiple times.
- That is, each occurrence of Ret is replaced with a code sequence to pop the
- saved registers, and preform the 3-byte Ret # instruction. Therefore, you
- should always use a single exit point in a routine, and jump to that when
- you need to exit from more than one place.
-
-
- CALLING INTERRUPTS
- ==================
-
- Chapter 11 explained how interrupts work, and mentioned that only assembly
- language can call an interrupt directly. An assembler program uses the Int
- instruction, and this tells the 8088 to look in the interrupt vector table
- in low memory to obtain the interrupt procedure's segment and address.
- Then the procedure is called as if it were a conventional subroutine.
- All of the DOS and BIOS services are accessed using interrupts, though
- there are so many different services that you also have to pass a service
- number to many of them. Most of the DOS services are accessed through
- interrupt 21h. Where BASIC uses the &H prefix to indicate a hexadecimal
- value, assembly language uses a trailing letter H. If you specify a number
- without an H it is assumed by MASM to be regular decimal. Note that MASM
- doesn't care if you use upper- or lowercase letters, and knows that either
- means hexadecimal.
- When specifying hexadecimal values to MASM, the first character must
- always be a digit. That is, 1234h is acceptable, but &HB800 must be
- entered as 0B800h. Using B800h will generate a syntax error.
-
-
- DOS AND BIOS SERVICES
-
- You have already seen how to call the BIOS routine that prints the screen
- and the DOS routine that returns the current drive. Let's continue and see
- how to call some of the other useful routines in the BIOS and DOS.
- The next example program, DosVer, shows how to call the DOS service that
- returns the DOS version number. Like many of the assembler routines that
- you can use with BASIC, DosVer relies on an existing DOS service to do the
- real work. In this program you will also learn how to push and pop values
- on the stack.
- The syntax for DosVer is CALL DosVer(Version%), where Version% returns
- with the DOS version number times 100. That is, if your PC is running DOS
- version 3.30, then Version% will be assigned the value 330. Manipulating
- floating point numbers is much more difficult than integers, and the added
- complexity is not justified for this routine.
- The DOS service that retrieves the version number returns with two
- separate values--the major version number (3 in this case) and the minor
- number (30). These values are returned in AL and AH respectively. The
- strategy here is to first multiply AL by 100, and then add AH. The last
- step is to assign the result to the incoming parameter Version%.
- Unfortunately, when you use AL for multiplication, the value 100 must
- be in a register or memory location. You can't just use MUL AL,100 though
- it would sure be nice if you could. Further, whenever AL is multiplied the
- result is placed into the entire AX register. Therefore, DosVer also uses
- BX to temporarily store the original contents of AX before the two are
- added together.
- As you already have learned, the only register that can be multiplied
- is AX, or its low-byte portion, AL. MASM knows if you plan to multiply AX
- or AL based on the size of the argument. For example, Mul BX means to
- multiply AX by BX and leave the result in DX:AX. Mul CL instead multiplies
- AL by CL and leaves the answer in AX.
- The complete DosVer routine is shown following, and comments explain
- each step.
-
-
- ;DOSVER.ASM, retrieves the DOS version number
-
- .Model Medium, Basic
- .Code
-
- DOSVer Proc, Version:Word
-
- Mov AH,30h ;service 30h gets the version
- Int 21h ;call DOS to do it
-
- Push AX ;save a copy of the version for later
- Mov CL,100 ;prepare to multiply AL by 100
- Mul CL ;AX is now 300 if running DOS 3.xx
-
- Pop BX ;retrieve the version, but in BX
- Mov BL,BH ;put the minor part into BL for adding
- Mov BH,0 ;clear BH, we don't want it anymore
- Add AX,BX ;add the major and minor portions
-
- Mov BX,Version ;get the address for Version%
- Mov [BX],AX ;assign Version% from AX
- Ret ;return to BASIC
-
- DOSVer Endp
- End
-
-
- Notice the extra switch that is done with BH and BL. AX is saved onto the
- stack because multiplying the byte in AL leaves the result as a full word
- in AX, thus destroying AH. When the version is popped into BX, the minor
- part is in BH. But you are not allowed to add registers that are different
- sizes (AX and BH). Further, any number in the high half of a register is
- by definition 256 times the value of the same number in a low half.
- Therefore, BH is first copied to BL to reflect its true value. BH is then
- cleared so it won't affect the result, and finally AX and BX are added.
- A better way to save AX and then restore it to BX would be to simply use
- Mov BX,AX immediately after the call to Interrupt 21h. I used Push and Pop
- just to show how this is done. As you can see, it is not necessary to pop
- the same register that was pushed. However, every Push instruction must
- always have a corresponding Pop, to keep the stack balanced. If a register
- or other value is on the stack when the final Ret is encountered, that
- value will be used as the return address which is of course incorrect.
- Division also acts on AX, or the combination of DX:AX. When you use
- the command Div BL, the 8088 knows you want to divide AX because BL is a
- byte-sized argument. It then leaves the result in AL and the remainder,
- if any, is placed into AH. Similarly, Div DX means that you are dividing
- the long integer in DX:AX, because DX is a word. The result of this
- division is assigned to AX, with the remainder in DX.
-
-
- ACCESSING BASIC STRINGS IN ASSEMBLY LANGUAGE
- ============================================
-
- As Chapter 2 explained, strings are stored very differently than regular
- numeric variables. BASIC lets you find the address of any variable with
- the VARPTR function. For integer or floating point numbers, the value
- VARPTR returns is the address of the actual data. But for strings, VARPTR
- instead returns the address of a string descriptor.
- DOS employs a different method entirely for its strings, using a CHR$(0)
- to mark the end. This is describes separately later in the section "DOS
- Strings."
-
-
- BASIC NEAR STRINGS
-
- A BASIC string descriptor is a table containing information about the
- string--that is, its length and address. In Microsoft compiled BASIC a
- string descriptor is comprised of two words of information. For QuickBASIC
- and near strings when using BASIC PDS, the first word contains the length
- of the string and the second holds the address of the first character.
- Consider the following BASIC instructions:
-
- X$ = "Assembler"
- V = VARPTR(X$)
-
- V now holds the starting address of the four-byte descriptor for X$. For
- the sake of argument, let's say that V is now 1234. Addresses 1234 and
- 1235 will together contain the length of X$ which is 9, and addresses 1236
- and 1237 will contain yet another address--that of the first character in
- X$. You can therefore find the length of X$ using this formula:
-
- Length = PEEK(V) + 256 * PEEK(V + 1)
-
- And the first character "A" can be located with this:
-
- Addr = PEEK(V + 2) + 256 * PEEK(V + 3)
-
- You could then print the string on the screen like this:
-
- FOR C = Addr TO Addr + 8
- PRINT CHR$(PEEK(C));
- NEXT
-
- Therefore, this is a BASIC model for how strings are located by an assembly
- language program. When you call an assembler routine with a string
- argument, BASIC first pushes the address of the descriptor onto the stack,
- before calling the routine. The next example is called Upper, because it
- capitalizes all of the characters in a string. Even though BASIC offers
- the UCASE$ and LCASE$ functions, these are relatively slow because they
- return a copy of the data that has been manipulated. Upper instead
- capitalizes the data in place very quickly.
- The strategy is to first get the descriptor address from the stack.
- Then Upper puts the length into BX and the address of the string data into
- SI. Upper steps through the string starting at the end, decrementing BX
- by one for each character. When BX crosses zero, it is done. A BASIC
- version is shown first, followed by the assembly language equivalent.
-
- Upper in BASIC:
-
- SUB Upper(Work$) STATIC
-
- '-- load SI with the address of Work$ descriptor
- SI = VARPTR(Work$)
-
- '-- assign LEN(Work$) to BX
- BX = PEEK(SI) + 256 * PEEK(SI + 1)
-
- '-- the address of the first character goes in SI
- SI = PEEK(SI + 2) + 256 * PEEK(SI + 3)
-
- More:
- BX = BX - 1 'point to the end of Work$
- IF BX < 0 GOTO Exit 'no more characters to do
- AL = PEEK(SI + BX) 'get the current character
- IF AL < ASC("a") GOTO More 'skip conversion if too low
- IF AL > ASC("z") GOTO More 'or if too high
- AL = AL - 32 'convert to upper case
- POKE SI + BX, AL 'put character back in Work$
- GOTO More 'go do it all again
-
- Exit: 'return to caller
-
- END SUB
-
-
- Upper in assembly language:
-
- Upper Proc, Work:Word
-
- Mov SI,Work ;load SI with Work$'s descriptor address
- Mov BX,[SI] ;put LEN(Work$) into BX
- Mov SI,[SI+2] ;SI holds address of the first character
-
- Next:
- Dec BX ;point to the next prior character
- Js Exit ;if sign is negative BX is less than 0
- Mov AL,[BX+SI] ;put the current character into AL
- Cmp AL,"a" ;compare it to ASC("a")
- Jb More ;jump if below to More
- Cmp AL,"z" ;compare AL to ASC("z")
- Ja More ;jump if above to More
- Sub AL,32 ;convert AL to upper case
- Mov [BX+SI],AL ;put AL back into Work$
- Jmp More ;jump to More
-
- Exit:
- Ret ;return to BASIC
-
- Upper Endp
- End
-
- What's Your Sign?
-
- Notice that for expediency, these routines work backwards from the end of
- the string. There are a number of shortcuts that you can use in assembly
- language, and one important one is being able to quickly test the result
- of the most recent numeric operation. If the program worked forward
- through the string, it would take three lines of code to advance to the
- next character, and also require saving the string length separately:
-
- Inc BX ;point to the next character
- Cmp BX,Length ;are we done yet?
- Jne More ;no, continue
-
- Notice the use of a new form of conditional jump--Js which stands for *Jump
- if Signed*. Here the code tests the sign of the number in BX, and jumps
- if it is negative. Though I haven't mentioned this yet, a conditional jump
- doesn't always have to follow a compare. Although a comparison will set
- the flags in the 8088 that indicate whether a particular condition is true,
- so will several other instructions. Some of these are Add, Sub, Dec, and
- Inc, but not Mov. So instead of having to include an explicit comparison:
-
- Dec BX ;decrement BX
- Cmp BX,0 ;compare it to zero
- Jl More ;jump if less to More
-
- All that is really needed is this:
-
- Dec BX
- Js More
-
- The Dec instruction sets the Sign Flag automatically, just as if a separate
- compare had been performed.
-
-
- Conditional Jump Instructions
-
- Besides Je, Jne, and Js, there are a few other forms of conditional jump
- instructions you should understand. Figure 12-6 lists all of the ones you
- are likely to find useful.
-
-
- Command Meaning
- ═══════ ══════════════════════════════════════
- Je Jump if equal
- Jne Jump if not equal
- Ja Jump if above (unsigned basis)
- Jna Jump if not above (unsigned basis)
- Jb Jump if below (unsigned basis)
- Jnb Jump if not below (unsigned basis)
- Jg Jump if greater (signed basis)
- Jng Jump if not greater (signed basis)
- Jl Jump if less (signed basis)
- Jnl Jump if not less (signed basis)
- Jc Jump if Carry Flag is set
- Jnc Jump if Carry Flag is clear
- Js Jump if sign flag is set
- Jns Jump if sign flag is not set
- Jcxz Jump if CX is zero
-
- Figure 12-6: The 8088 conditional jump instructions.
-
-
- You should know that Je and Jne also have an alias command name: Jz and
- Jnz. These stand for *Jump if Zero* and *Jump if Not Zero* respectively,
- and they are identical to Je and Jne. In fact, though I didn't mention
- this earlier, the Repe and Repne string repeat prefixes are sometimes
- called Repz and Repnz.
- Because Je and Jz cause MASM to generate the identical machine code
- bytes, they may be used interchangeably. In some cases you may want to use
- one instead of the other, depending on the logic in your program. For
- example, after comparing two values you would probably use Je or Jne to
- branch if they are equal or not equal. But after testing for a zero or
- non-zero value using Or AX,AX you would probably use Jz or Jnz. This is
- really just a matter of semantics, and either version can be used with the
- same results.
- Also, please understand that Jnb is not the same as Ja. Rather, the
- case of being Not Below is the same as being Above Or Equal. In fact, MASM
- recognizes Jae (Jump if Above or Equal) to mean the same thing as Jnb.
- Likewise, Jbe (Jump if Below or Equal) is the same as Jna, Jge (Jump if
- Greater or Equal) is the same as Jnl, and Jle (Jump if Less or Equal) is
- identical to Jng. Again, which form of these instructions you use will
- depend on how you are viewing the data and comparisons.
- Note the special form of conditional jump, Jcxz. Jcxz stands for Jump
- if CX is Zero, and it combines the effects of Cmp CX,0 and Je label into
- a single fast instruction. Jcxz is also commonly used prior to a Loop
- instruction. When you use Loop to perform an operation repeatedly, CX must
- be assigned initially to the number of times the loop is to be executed.
- But if CX is zero the loop will execute 65536 times! Thus, adding Jcxz
- Exit avoids this undesirable behavior if zero was passed accidentally.
- Finally, you must be aware that a conditional jump cannot be used to
- branch to a label that is more than 128 bytes earlier, or 127 bytes farther
- ahead in the code. A condition jump instruction is only two bytes, with
- the first indicating the instruction and the other holding the branch
- distance. If you need to jump to a label farther away than that you must
- reverse the sense of the condition, and jump to a near label that skips
- over another, unconditional jump:
-
- Cmp AX,BX ;we want to jump to Label: if AX is greater
- Jna NearLabel ;so jump to NearLabel if it's NOT greater
- Jmp Label ;this goes to Label: which is farther away
- NearLabel:
- .
- .
-
- As used here, the unconditional Jmp instruction can branch to any location
- within the current code segment. There is also a short form of Jmp, which
- requires only two bytes of code instead of three. If you are jumping
- backwards in the program and the address is within 128 bytes, MASM uses the
- shorter form automatically. But if the jump is forward, you should specify
- Short explicitly: Jmp Short Label. Some non-Microsoft assemblers do not
- require you to specify Short; the newest MASM version 6.x also adjusts its
- generated code to avoid the extra wasted byte.
-
-
- DOS STRINGS
-
- When string information is passed to a DOS routine, for example when giving
- a file or directory name, the string must end with a CHR$(0). In DOS
- terminology this is called an ASCIIZ string. (Do not confuse this with a
- CHR$(26) Ctrl-Z which marks the end of a file.) Unlike BASIC, DOS does
- not use string descriptors, so this is the only way DOS can tell when it
- has reached the end. By the same token, when DOS returns a string to a
- calling program, it marks the end with a trailing zero byte.
- When passing a string to a DOS service from BASIC you must either
- concatenate a CHR$(0) manually, or add extra code within the assembler
- routine to copy the name into local storage and add a zero byte to the
- copy. From BASIC you would therefore use something like this:
-
- CALL Routine(FileName$ + CHR$(0))
-
-
- BASIC FIXED-LENGTH STRINGS
-
- Fixed-length strings and the string portion of a TYPE variable do not use
- a string descriptor, which you might think would require a different
- strategy to access them. But whenever a fixed-length string is used as an
- argument to an assembler routine or BASIC subprogram, BASIC first copies
- it into a temporary conventional string, and it is the temporary string
- that is passed to the routine. When the routine returns, BASIC copies the
- characters back into the original fixed-length string. Thus, any routine
- written in assembly language that expects a descriptor will work correctly,
- regardless of the type of string being sent.
- Of course, this copying requires BASIC to generate many extra bytes of
- assembler code for each call. If you do not want BASIC to create a
- temporary string copy from one of a fixed-length, you must first define the
- string as a TYPE like this:
-
- TYPE Flen
- S AS STRING * 20
- END TYPE
- DIM FString AS FLen
-
- Though this appears to be the same as defining FString as a string with a
- fixed length of 20, there is an important difference: declaring it as a
- TYPE tells BASIC not to make a copy. That is, BASIC does not treat FString
- as a string, as long as the ".S" portion that identifies it as a string is
- not used. Here's an example based on the FLen TYPE that was defined above:
-
- DIM FString AS FLen 'FString is a TYPE variable
- FString.S = "This is a test" 'assign the string portion
- CALL Routine(FString) 'call the routine without .S
-
- Here, the address of the first character in the string is passed to the
- routine, as opposed to the address of a temporary string descriptor. We
- have told BASIC to call Routine, and pass it the entire FString TYPE but
- without interpreting the .S string component. This next example does cause
- BASIC to create a temporary copy:
-
- CALL Routine(FString.X)
-
- The short assembly language routine that follows expects the address of a
- fixed-length string with a length of 20, as opposed to the address of a
- string descriptor. The routine then copies the characters to the
- upper-left corner of a color monitor.
-
-
- Push BP ;access the stack as usual
- Mov BP,SP
- Mov SI,[BP+6] ;SI points to the first character
- Mov DI,0 ;the first address in screen memory
- Mov AX,0B800h ;color monitor segment when in text mode
- Mov ES,AX ;move into ES through AX
- Mov CX,20 ;prepare to copy 20 characters
- Cld ;clear the direction flag to copy forward
-
- More:
- Movsb ;copy a byte to screen memory
- Inc DI ;skip over the attribute byte
- Loop More ;loop until done
- Pop BP ;restore BP
- Ret 2 ;return to BASIC
-
-
- Recall that the color monitor segment value of 0B800h must be assigned to
- ES through AX, because it is not legal to assign a segment register from
- a constant. Also, notice the way that DI is cleared to zero. Although Mov
- DI,0 indeed moves a zero into DI, this is not the most efficient way to
- clear a register. Any time a numeric value is used in a program (0 in this
- case), that much extra space is needed to store the actual value as part
- of the instruction. A preferred method for clearing a register is with
- the Xor instruction. That is, Xor DI,DI gives the same result as Mov DI,0
- except it is one byte shorter and slightly faster.
- When Xor is performed on any two values, only those bits that are
- different are set to 1. But since the same register is used here for both
- operands, all of the result bits will be cleared to 0. The code for using
- Xor is decidedly less obvious, but you'll see Xor used this way very often
- in assembly listings in magazines and books. Another, equally efficient
- way to clear a register is to subtract it from itself using Sub AX,AX.
-
-
- FAR STRINGS IN BASIC PDS
-
- Accessing near strings in QuickBASIC and BASIC PDS is a relatively simple
- task, because both the descriptor and the string data are known to be in
- near DGROUP memory. But BASIC PDS also supports far strings, where the
- data may be in a different segment. The composition of a far string
- descriptor was shown in Chapter 2; however, you do not need to manipulate
- these descriptors yourself directly.
- BASIC PDS includes two routines--StringLength and StringAddress--that
- do the work of locating far strings for you. Further, because Microsoft
- could change the way far strings are organized in the future, it makes the
- most sense to use the routines Microsoft supplies. If the layout of far
- string descriptors changes, your program will still work as expected.
- StringLength and StringAddress expect the address of the string
- descriptor, and they return the string's length and segmented address
- respectively. Note that while far string data may be in nearly any
- segment, the descriptors themselves are always in DGROUP. Also note that
- these routines are not very well-behaved. In particular, registers you may
- be using are changed by the routines. To solve this problem and also to
- let you get all of the information in a single call, I have written the
- StringInfo routine. StringInfo is contained in the FAR$.ASM file on the
- accompanying disk.
-
- ;from an idea originally by Jay Munro
- .Model Medium, Basic
- Extrn StringAddress:Proc ;these are part of PDS
- Extrn StringLength:Proc
-
- .Code
- StringInfo Proc Uses SI DI BX ES
-
- Pushf ;save the flags manually
-
- Push ES ;save ES for later
- Push SI ;pass incoming descriptor
- Call StringAddress ;call the PDS routine
-
- Pop ES ;restore ES for StringLength
- Push AX ;save offset and segment
- Push DX ; returned by StringAddress
-
- Push SI ;pass incoming descriptor
- Call StringLength ;get the length
- Mov CX,AX ;copy the length to CX
-
- Pop DX ;retrieve the saved Segment
- Pop AX ;and the address
-
- Popf ;restore the flags manually
- Ret ;restore registers and return
-
- StringInfo Endp
- End
-
- StringInfo is called with DS:SI pointing to the string descriptor, and it
- returns the length in CX and the address of the string data in DX:AX.
- Although StringInfo could be designed to return the segment in DS or ES,
- it is safer to assign the segment registers yourself manually.
- Notice the Uses clause--this tells MASM that the named registers must
- be preserved, and generates additional code to push those registers upon
- entry to the procedure, and pop them again upon exit.
- Also notice the new Extrn directive at the beginning of the source file.
- These tell the assembler that the stated routines are not in the current
- source file. MASM then places the external name in the object file header,
- with instructions to LINK to fill in the address portion of the Call. Data
- must also be declared as external if it is not in the same source file as
- the routine being assembled. When a data item is to be made available to
- other modules, you must also have a corresponding Public statement in that
- file for the same reason:
-
- .Model Medium, Basic
- .Data
- Public MyData
- MyData DW 12345
- .
- .
-
-
- ACCESSING ARRAYS
- ================
-
- As you have seen, a conventional variable is passed to an assembly language
- subroutine by placing its address onto the stack. If the variable is a
- string, then the address passed is that of its descriptor, and the string
- data address is read from there. Accessing array elements is only slightly
- more involved, because array elements are always stored in adjacent memory
- locations. Let's look first at integer arrays.
- When BASIC encounters the statement DIM X%(100) in your program, it
- allocates a contiguous block of memory 202 bytes long. (Unless you first
- used the statement OPTION BASE 1, dimensioning an array to 100 means 101
- elements.) The first two bytes in this block hold the data for X%(0), the
- next two bytes hold X%(1), and so forth. When you ask VARPTR to find
- X%(0), the address it returns is the start of this block of memory.
- The address of subsequent array elements may then be easily computed
- from this base address. But with a dynamic array, the segment that holds
- the array may not be the same as the segment where regular variables are
- stored. Also, huge arrays that span more than 64K require extra care when
- crossing a 64K segment boundary.
- String arrays are structured in a similar fashion, in that each element
- follows the previous one in memory. For each string array element that is
- dimensioned, four bytes are set aside. These bytes comprise a table of
- descriptors which contain the length and address words for each element in
- the array. But the important point is that once you know where one element
- or string descriptor is located, it is easy to find all of those that are
- adjacent. Following is a QuickBASIC example that shows how to locate
- Array$(15), based on the VARPTR address of Array$(0).
-
-
- DIM Array$(100)
- Array$(15) = "Find me"
-
- Descriptor = VARPTR(Array$(0))
- Descriptor = Descriptor + (4 * 15)
-
- Length = PEEK(Descriptor) + 256 * PEEK(Descriptor + 1)
- PRINT "Length ="; Length
-
- Addr = PEEK(Descriptor + 2) + 256 * PEEK(Descriptor + 3)
- PRINT "String = ";
- FOR X = Addr TO Addr + Length - 1
- PRINT CHR$(PEEK(X));
- NEXT
-
-
- DYNAMIC ARRAYS
-
- Most of the routines shown so far manipulated variables that are located
- in near memory. BASIC can store numeric, TYPE, and fixed-length string
- arrays in far memory, and additional steps are needed to read from and
- write to those arrays.
- When an assembly language routine receives control after a call from
- BASIC, it can access your regular variables because they are in the default
- data segment. Most memory accesses assume the data is in the segment held
- in the DS register. For example, the statement Mov [BX],AX assigns the
- value in AX to the memory location identified by BX within the segment held
- in DS. Likewise, Sub [DI+10],CX subtracts the value held in CX from the
- memory address expressed as DI+10, where that address is again in the
- default data segment.
- It is also possible to specify a segment other than the current default.
- One way is with a *segment override* command, like this:
-
- Mov ES:[BX],AX
-
- Here, the segment held in ES is used instead of DS. A segment override
- adds only one byte of code, so it is quite efficient. If you plan to
- access data in a different segment many times, you can optionally set DS
- to that segment. However, it is mandatory that you reset DS to its
- original value before returning to BASIC. You must also understand that
- changing DS means you no longer have direct access to DGROUP anymore. In
- that case you could use the stack segment as an override, since the stack
- segment is always the same as the data segment in a BASIC program. The
- next short example shows this in context.
-
- Push DS ;save DS
- Mov DS,FarSegment ;now DS points to your far data
- . ;access that far data here
- .
- Mov AX,SS:[Variable] ;access Variable in DGROUP
- . ;access more far data here
- Pop DS ;restore DS before returning
-
- When Microsoft introduced QuickBASIC version 2.0, one of the most exciting
- new features it offered was support for dynamic numeric arrays. Unlike
- QuickBASIC near strings, string arrays, and non-array variables, these
- arrays are always located outside of BASIC's near 64K data segment. This
- means that an assembler routine needs some way to know both the address and
- the segment for an array element that is passed to it.
- In general, routines you design that work on an entire array will be
- written to expect a particular starting element. The routine can then
- assume that all of the subsequent elements lie before or after it in
- memory. Unfortunately, this does not always work unless you add extra
- steps. If you call an assembly language routine passing one element of a
- far-memory dynamic array like this:
-
- CALL Routine(Array(1))
-
- BASIC makes a copy of the array element into a temporary variable in near
- memory, and then passes the address of that copy to the routine. Thus,
- while the routine can still receive an array element's value, it has no way
- to determine its true address. And without the address, there is no way
- to get at the rest of the array.
- Since being able to pass an entire array is obviously important, BASIC
- supports two options to the CALL command--SEG and BYVAL. The SEG keyword
- indicates that both the address and the segment are to be passed on the
- stack, and it also tells BASIC not to make a copy of the array element.
- SEG is used with an array element (or any variable, for that matter) like
- this:
-
- CALL Routine(SEG Array%(1))
-
- You could also send the segment and address manually, like this:
-
- CALL Routine(BYVAL VARSEG(Array%(1)), BYVAL VARPTR(Array%(1)))
-
- In both cases, BASIC first pushes the segment where the element resides
- onto the stack, followed by the element's address within that segment. By
- pushing them in this order the routine can conveniently use either Lds
- (Load DS) or Les (Load ES) to get both the segment and address in one
- operation:
-
- Les DI,[BP+6] ;if using manual stack addressing
- or
- Les BX,[StackArg] ;if using MASM's simplified directives
-
- Les loads four bytes in one operation, placing the lower word at [BP+6]
- into the named register (DI in the first example case), and the higher word
- at [BP+8] into ES. Lds works the same, except the higher word is instead
- moved into DS. Once the segment and address are loaded, you can access all
- of the array elements:
-
- Push DS ;save DS
- Lds SI,[BP+6] ;now DS:SI points at first element
- Mov [SI],AX ;assign Array%(1) from AX
- Add SI,2 ;now SI points at the next element
- Mov [SI],BX ;assign Array%(2) from BX
- Pop DS ;restore DS
- . ;continue
- .
-
- If Les were used instead of Lds, then an ES: override would be needed to
- assign the elements. Although you must always preserve the contents of
- DS regardless of the version of BASIC, some registers need to be saved only
- when using BASIC PDS far strings. Other registers do not need to be saved
- at all. Figure 12-7 shows which registers must be preserved based on the
- version of BASIC.
-
-
- QuickBASIC and BASIC PDS
- PDS near strings far strings
- ═══════════════ ══════════
- DS DS
- SS SS
- BP BP
- SP SP
- ES
- SI
- DI
-
- Figure 12-7: The registers that must be preserved in an assembly language
- subroutine.
-
-
- Besides having to save and restore the registers shown in Figure 12-7, you
- must also be sure that the Direction Flag is cleared to forward before
- returning to BASIC. The Direction Flag affects the 8088 string operations,
- and is by default set to forward. You can usually ignore the direction
- flag unless you set it to backwards explicitly with the Std instruction.
- In that case, you must use a corresponding Cld command.
-
-
- Huge Arrays
-
- A huge array is one that spans more than one 64K segment, and as you can
- imagine, it requires extra steps to access all of the elements. That is,
- the assembler routine must know which elements are in what segment, and
- manually load those segments as needed. The following code fragment shows
- how to walk through all of the elements in a huge integer array, and just
- for the sake of the example adds each element to determine the sum of all
- of them.
- A simple setup example and call syntax for this routine is as follows:
-
- REDIM Array&(1 TO 30000)
- FOR X% = 1 TO 30000
- Array&(X%) = X%
- NEXT
-
- CALL SumArray(SEG Array&(1), 30000, Sum&)
- PRINT "Sum& ="; Sum&
-
-
- And here's the code for the SumArray routine:
-
- .Model Medium, Basic
- .Code
-
- SumArray Proc Uses SI, Array:DWord, NumEls:Word, Sum:Word
-
- Push DS ;save DS so we can restore it later
- Push SI ;PDS far strings require saving SI too
-
- Xor AX,AX ;clear AX and DX which will accumulate
- Mov DX,AX ; the total
-
- Mov BX,NumEls ;get the address for NumElements%
- Mov CX,[BX] ;read NumElements% before changing DS
- Lds SI,Array ;load the address of the first element
- Jcxz Exit ;exit if NumElements = 0
-
- Do:
- Add AX,[SI] ;add the value of the low word
- Adc DX,[SI+2] ;and then add the high word
- Add SI,4 ;point to the next array element
-
- Or SI,SI ;are we beyond a 32k boundary?
- Jns More ;no, continue
-
- Sub SI,8000h ;yes, subtract 32k from the address
- Mov BX,DS ;copy DS into BX
- Add BX,800h ;adjust the segment to compensate
- Mov DS,BX ;copy BX back into DS
-
- More:
- Loop Do ;loop until done
-
- Exit:
- Pop SI ;restore SI for BASIC
- Pop DS ;restore DS and gain access to Sum&
- Mov BX,Sum ;get the DGROUP address for Sum&
- Mov [BX],AX ;assign the low word
- Mov [BX+2],DX ;and then the high word
-
- Ret ;return to BASIC
-
- SumArray Endp
- End
-
- The segment bounds checking is handled by the six lines that start with
- Or SI,SI. The idea is to see if the address is beyond 32767, subtract
- 32768 if it is, and then adjust the segment to compensate. The most direct
- way would have been with Cmp SI,32767 and then Ja More, but Cmp used this
- way generates three bytes of code, whereas Or creates only two bytes.
- Since Or sets the Sign flag if the number is negative (above 32767), you
- can use it to know when the address adjustment is needed.
- Because it is not legal to add or subtract a segment register, DS is
- first copied to BX, 800h is added to that, and the result is then copied
- back to DS. 800h is used instead of 8000h (32768) because a new segment
- begins every 16 bytes. [That is, adding 800h to a segment value is the
- same as adding 8000h to the address.]
- SumArray also introduces a new instruction: Adc means Add with Carry,
- and it is used to add long integer values that by definition span two
- words. When you add two registers--say, AX and BX--if the result exceeds
- 65535 only the remainder is saved. However, the Carry Flag is set to
- indicate the overflow condition. Adc takes this into account, and adds
- one extra to its result if the Carry Flag is set. Therefore, whenever two
- long integers are added you'll use Add to combine the lower words, and Adc
- for the high words. Similarly, subtracting long integers requires that you
- use Sub to subtract the lower words and then Sbb (Subtract with Borrow) on
- the upper words.
- Although the details are hidden from you, when more than one parameter
- is passed to an assembly language routine it is the last in the list that
- is at [BP+6] on the stack. The previous argument is at [BP+8], and the one
- before that is at [BP+10]. Because the stack grows downward as new items
- are pushed onto it, each subsequent item is at a lower address.
- Finally, in a real program this routine would probably be designed as
- a function. Using a function avoids having to pass the Sum& parameter to
- receive the returned value, and helps reduce the size of the program.
-
-
- ASSEMBLER FUNCTIONS
- ===================
-
- Designing a procedure as a function lets you return information to a
- program, but without the need for an extra passed parameter. Functions are
- also useful because BASIC performs any necessary data type conversion
- automatically. For example, if you have written a function that returns
- an integer value, you can freely assign the result to a single precision
- variable.
- You can also test the result of a function directly using IF, display
- it directly with PRINT, or pass it as a parameter to another procedure.
- Some typical examples are shown here:
-
- SingleVar! = MyFunction%
-
- IF YourFunction&(Argument%) > 1004 THEN ...
-
- PRINT HisFunction$(Any$)
-
- Beginning with QuickBASIC version 4.0, functions written in assembly
- language may be added to a BASIC program. To have a function return an
- integer value, simply place the value into the AX register before returning
- to BASIC. If the function is to return a long integer, both DX and AX are
- used. In that case, DX holds the higher word and AX holds the lower one.
-
-
- STRING FUNCTIONS
-
- String functions are only slightly more complicated to design. A string
- function also uses AX as a return value, but in this case AX holds the
- address of a string descriptor you have created. The complete short string
- function that follows accepts an integer argument, and returns the string
- "False" if the argument is zero or "True" if it is not.
-
- ;Syntax:
- ;DECLARE FUNCTION TrueFalse$(Argument%)
- ;Answer$ = TrueFalse$(Argument%)
-
- .Model Medium, Basic
- .Data
- DescLen DW 0
- DescAdr DW 0
- True DB "True"
- False DB "False"
-
- .Code
- TrueFalse Proc, Argument:Word
-
- Mov DescLen,4 ;assume true
- Mov DescAdr,Offset True
-
- Mov BX,Argument ;get the address for Argument%
- Cmp Word Ptr [BX],0 ;is it zero?
- Jne Exit ;no, so we were right
- Inc DescLen ;yes, return five characters
- Mov DescAdr,Offset False ;and the address of "False"
-
- Exit:
- Mov AX,Offset DescLen ;show where the descriptor is
- Ret ;return to BASIC
-
- TrueFalse Endp
- End
-
- Although the function is declared using a dollar sign in the name, the
- actual procedure omits that. [The dollar sign merely tells BASIC what type
- of information will be returned. It is not part of the actual procedure
- name.] TrueFalse begins by defining a string descriptor in the .Data
- segment. It is also possible to store strings and other data in the code
- segment and access it with a CS: segment override. However, data that is
- returned as a function must be in DGROUP, and so must the descriptor.
- The first two statements assign the descriptor to an output string
- length of four characters, and the address of the message "True". Then,
- the address of Argument is obtained from the stack, and its value is
- compared to zero. If it is not zero, then the descriptor is already
- correct and the function can proceed. Otherwise, the descriptor length is
- incremented to reflect the correct length, and the address portion is
- reassigned to show where the string "False" begins in memory. In either
- case, the final steps are to load AX with the address of the descriptor,
- and then return to BASIC.
- MASM also lets you access data using simple arithmetic. For example,
- the descriptor could have been defined as a single pair of words with one
- name, and the second word could be accessed based on the address of the
- first one like this:
-
- .Data
- Descriptor DW 0, 0
- True DB "True"
- False DB "False"
-
- .Code
- .
- .
- Inc Descriptor
- Mov Descriptor+2,Offset False
- .
- .
-
-
- Far String Functions
-
- Far string functions require more work to write than near string functions,
- because of the added overhead needed to support far strings. Fortunately,
- BASIC includes routines that simplify the task for you. Actually, the
- routines to create and assign strings have always been included; it's just
- that Microsoft never documented how to do it before BASIC 7.0. Later in
- this chapter I'll show code to create strings that works with all versions
- of BASIC 4.0 or later.
- The StringAssign routine expects six arguments on the stack, for the
- segment, address, and length of both the source and destination strings.
- StringAssign can assign from or to any combination of fixed- and variable-
- length strings. If the length argument for either string is zero, then
- StringAssign knows that the address is that of a descriptor. Otherwise,
- the address is of the data in a fixed-length string.
- Because of the added overhead of obtaining values and pushing them on
- the stack, I have created a short wrapper program that does this for you.
- MakeString accepts the same arguments as StringAssign, but they are passed
- using registers rather than on the stack. Of course, calling one routine
- that in turn calls another takes additional time. But the savings in code
- size when MakeString is called repeatedly will overshadow the very slight
- additional delay.
- MakeString is called with DX:AX holding the segmented address of the
- source string, and CX holding its fixed length. If the source is a
- conventional string, CX is set to zero to indicate that. The destination
- address is identified with DS:DI, using BX to hold the length. Again, BX
- holds zero if the destination is not a fixed-length string.
-
-
- ;from an idea originally by Jay Munro
- .Model Medium, Basic
- Extrn STRINGASSIGN:Proc
-
- .Code
- MakeString Proc Uses DS
-
- Push DX ;push the segment of the source string
- Push AX ;push the address of the source string
- Push CX ;push the string length
- Push DS ;push the segment of the destination
- Push DI ;push the address of the destination
- Push BX ;push the destination length
-
- Call STRINGASSIGN ;call BASIC to assign the string
- Ret
-
- MakeString Endp
- End
-
-
- Now, with the assistance of MakeString, TrueFalse$ can be easily modified
- to work with BASIC 7 far strings:
-
- .Model Medium, Basic
- Extrn MakeString:Proc ;this is in FAR$.ASM
-
- .Data
- Descriptor DW 0, 0 ;the output string descriptor
- True DB "True"
- False DB "False"
-
- .Code
- TrueFalse Proc Uses ES DS SI DI, Argument:Word
-
- Mov CX,4 ;assume true
- Mov AX,Offset True
-
- Mov BX,Argument ;get the address for Argument%
- Cmp Word Ptr [BX],0 ;is it zero?
- Jne @F ;no, so we were right
-
- Inc CX ;yes, assign five characters
- Mov AX,Offset False ;and use the address of "False"
-
- @@:
- Mov DX,DS ;assign the segment and address
- Mov DI,Offset Descriptor ; of the destination descriptor
- Xor BX,BX ;assign to a descriptor
- Call MakeString ;let MakeString do the work
-
- Mov AX,DI ;AX = address of output descriptor
- Ret ;return to BASIC
-
- TrueFalse Endp
- End
-
- Notice the introduction of the new at-symbol (@) assembler directive. The
- at-symbol and double at-symbol label are quite useful, because they let you
- avoid having to create unique label names each time you specify the target
- of a jump. As with BASIC, creating many different label names is a
- nuisance, and also impinges on the assembler's working memory. When a
- label is defined using @@: as a name, you can jump forward to it using @F
- or backwards using @B. Multiple @@: labels may be used in the same
- program, and @F and @B always branch to the nearest one in the stated
- direction.
-
-
- FLOATING POINT FUNCTIONS
-
- Single and double precision functions are handled in yet another manner.
- Although a single precision value could be returned in the DX:AX register
- combination, a double precision result would need four registers, which is
- impractical. Further, a floating point number is most useful to BASIC if
- it is stored in a memory location, rather than in registers.
- When BASIC invokes a floating point function it adds an extra, dummy
- parameter to the end of the list of arguments you pass. If no parameters
- are being used, it creates one. This parameter is the address into which
- your routine is to place the outgoing result. Because of this added
- parameter, it is essential that you account for it when returning to BASIC.
- Thus, a function without arguments must use Ret 2, a function with one
- argument needs Ret 4, and so forth. Since we're using MASM's simplified
- directives, all that is needed is to create an extra parameter name.
- The short double precision function that follows squares a double
- precision number much faster than using Value# ^ 2, and also shows how to
- perform simple floating point math using assembly language. You will
- declare and invoke Square like this:
-
- DECLARE FUNCTION Square#(Variable#)
- Result = Square#(Variable#)
-
- ;SQUARE.ASM, squares a double precision number
- ;
- ;WARNING: This file must be assembled using /e (emulator).
-
- .Model Medium, Basic
- .Code
- .8087 ;allow 8087 instructions
-
- Square Proc, InValue:Word, OutValue:Word
-
- Mov BX,InValue ;get the address for InValue
- FLd QWord Ptr [BX] ;load InValue onto the 8087 stack
- FMul QWord Ptr [BX] ;multiply InValue by itself
-
- Mov BX,OutValue ;get the address for OutValue
- FStp QWord Ptr [BX] ;store the result there
- FWait ;wait for the 8087 to finish
-
- Mov AX,BX ;return DX:AX holding the full
- Mov DX,DS ; address of the output value
- Ret ;return to BASIC
-
- Square Endp
- End
-
- This Square function illustrates several important points. The first is
- the use of MASM's /e switch, which lets an assembly language routine share
- BASIC's floating point emulator. When a BASIC program begins, it looks to
- see if an 8087 coprocessor is installed in the host PC. If so, it uses one
- set of library routines; otherwise it uses another.
- The library routines that use an 8087 simply modify the caller's code
- to change the floating point interrupts that BASIC generates into actual
- 8087 instructions. It then returns to the instruction it just created and
- executes it. Although this adds to the time needed to perform a floating
- point operation, the code is patched only once. Thus, statements within
- a FOR or DO loop operate very quickly after the first iteration. This is
- very much like the method used by the BRUN library described in Chapter 1.
- When no coprocessor is detected, the floating point interrupts that
- BASIC generates are used to invoke routines in BASIC's floating point
- software emulator. As its name implies, an emulator imitates the behavior
- of a coprocessor using assembly language commands. A coprocessor can
- perform a variety of floating point operations, including addition,
- multiplication, and rounding, as well as some transcendental functions such
- as logarithms and arctangents.
- When you use the /e switch, MASM adds extra information to the object
- file header that tells LINK where to patch your 8087 instructions. LINK
- can then change your code to the equivalent floating point interrupts,
- similar to the way BASIC patches its own code to change the interrupts to
- 8087 instructions. Therefore, when you write floating point code that will
- be called from BASIC, your routine can tie into BASIC's emulator, and use
- it automatically if no coprocessor is installed.
- Also, notice the .8087 directive which tells MASM not to issue an error
- message when it sees those instructions. Other, similar directives are
- .80287 and .80387, and also .80286 and .80386. These directives inform
- MASM that you are intentionally using advanced commands that require these
- processors, and have not made a typing error.
- The actual body of the Square function is fairly simple. First, the
- address of the incoming value is retrieved from the system stack, and then
- the data at that address is loaded onto the coprocessor's stack using the
- FLd (Floating point Load) instruction. Since this is a double precision
- value, QWord Ptr (Quad Word Pointer) is needed to indicate the size of the
- data. Had the incoming value been single precision, DWord Ptr (Double
- Word Pointer) would be used instead. One important feature of an 8087 or
- software emulator is that a number may be converted from one numeric format
- to another simply by loading it as one data type, and then saving it as
- another.
- The next instruction, FMul (Floating point Multiply), multiplies the
- value currently on the 8087 stack by the same address. Since the original
- value is still present, there's no need to make a new copy. Next, the
- destination address is placed into BX, and the result now on the 8087 stack
- is stored there. The trailing letter p in the FStp instruction specifies
- that the value loaded earlier is to be popped from the coprocessor stack.
- A complete discussion of 8087 instructions and how the coprocessor stack
- operates goes beyond what I can hope to cover here. When in doubt about
- what instruction is needed, I suggest that you code a similar sample in
- BASIC, and then examine the code BASIC generates using CodeView. There are
- also several books that focus on writing floating point instructions in
- assembly language.
- The last 8087 instruction is FWait, and it tells the 8088 to wait until
- the coprocessor has finished, before continuing. Because an 8087 is a true
- coprocessor, it operates independently of the main 8088 CPU. Once a value
- is loaded and the 8087 is instructed to perform an operation, the 8087
- returns immediately to the program that issued the instruction and
- continues to process the numbers in the background. If Square exited
- immediately and BASIC read the returned value, there's a good chance that
- the 8087 did not finish and the value has not yet been stored! In that
- case, whatever happened to be in memory at that time would be the value
- that BASIC uses, which is obviously incorrect.
- Experienced 8087 programers know how long the various coprocessor
- instructions take to complete, and with careful planning the number of
- FWait commands can be kept to a minimum. However, the code that BASIC
- generates always finishes with an FWait. Of course, there is no need to
- wait when the emulator is in use. In fact, an FWait is patched by BASIC
- to do nothing (Mov AX,AX), rather than waste time invoking an empty
- interrupt handler repeatedly.
- As shown, Square can be added to a Quick Library for use with either
- QuickBASIC or BASIC PDS. Unfortunately, the information link needs to
- patch 8087 instructions is available only with BASIC PDS. Therefore, the
- following file is included in the libraries on the accompanying disk, to
- supply the external data that LINK requires.
-
-
- ;FIXUPS.ASM, deciphered by Paul Passarelli
-
- FIARQQ Equ 0FE32h
- FJARQQ Equ 04000h
- FICRQQ Equ 00E32h
- FJCRQQ Equ 0C000h
- FIDRQQ Equ 05C32h
- FIERQQ Equ 01632h
- FISRQQ Equ 00632h
- FJSRQQ Equ 08000h
- FIWRQQ Equ 0A23Dh
-
- Public FIARQQ
- Public FJARQQ
- Public FICRQQ
- Public FJCRQQ
- Public FIDRQQ
- Public FIERQQ
- Public FISRQQ
- Public FJSRQQ
- Public FIWRQQ
- End
-
-
- These values are added to the floating point instruction bytes during the
- linking process, and the addition converts those statements into equivalent
- BASIC floating point interrupt commands. For example, the 8087 statement
- Fld DWord Ptr [1234h] is represented in memory as the following series of
- Hexadecimal bytes:
-
- 9B D9 06 34 12
-
- After LINK adds the value FIDRQQ (5C32h) to the first two bytes of this
- command the result is:
-
- CD 35 06 34 12
-
- And when disassembled back to assembler mnemonics, the CD35h displays as
- Int 35h. The three bytes that follow are always left unchanged, and they
- specify the type of operation--DWord Ptr on a memory location--and the
- address of that location.
-
-
- Floating Point Comparisons
-
- At the core of any sorting or searching routine is an appropriate
- comparison function. Previous chapters showed how to compare string data,
- and as you can imagine comparing floating point values is much more
- complex. But now that you know how to tap into BASIC's floating point
- routines it is almost trivial to effect a floating point comparison. The
- routines that follow let you compare either single- or double precision
- values, by passing them as arguments.
-
- ;COMPAREFP.ASM, compares floating point values
-
- ;WARNING: This file must be assembled using /e (emulator)
-
- .Model Medium, Basic
- Extrn B$FCMP:Proc ;BASIC's FP compare routine
-
- .8087 ;allow coprocessor instructions
- .Code
-
- CompareSP Proc, Var1:Word, Var2:Word
-
- Mov BX,Var2 ;get the address of Var1
- Fld DWord Ptr [BX] ;load it onto the 8087 stack
- Mov BX,Var1 ;same for Var2
- Fld DWord Ptr [BX]
- FWait ;wait until the 8087 says it's okay
- Call B$FCMP ;compare the values, (and pop both)
-
- Mov AX,0 ;assume they're the same
- Je Exit ;we were right
- Mov AL,1 ;assume Var1 is greater
- Ja Exit ;we were right
- Dec AX ;Var1 must be less than Var2
- Dec AX ;decrement AX to -1
-
- Exit:
- Ret ;return to BASIC
-
- CompareSP Endp
-
-
-
- CompareDP Proc, Var1:Word, Var2:Word
-
- Mov BX,Var2 ;as above
- Fld QWord Ptr [BX]
- Mov BX,Var1
- Fld QWord Ptr [BX]
- FWait
- Call B$FCMP
-
- Mov AX,0
- Je Exit
- Mov AL,1
- Ja Exit
- Dec AX
- Dec AX
-
- Exit:
- Ret
-
- CompareDP Endp
- End
-
- Like the Compare3 function shown in Chapter 8, CompareSP and CompareDP are
- integer functions that return -1, 0, or 1 to indicate if the first value
- is less than, equal to, or greater than the second. Therefore, to use
- these from BASIC you would invoke them like this:
-
- IF CompareSP%(Value1!, Value2!) = -1 THEN
- 'the first value is smaller than the second
- END IF
-
- And to test if the first is equal to or greater than the second you would
- instead do this:
-
- IF CompareSP%(Value1!, Value2!) >= 0 THEN
- 'the first value is equal or greater
- END IF
-
- You can also use these functions from assembly language. But if you do
- this, I suggest a simple modification. A comparison routine meant to be
- called from another assembler routine would not generally return the result
- in the registers. Rather, it would leave the flags set appropriately for
- a subsequent Ja or Jne branch.
- Fortunately, BASIC's B$FCMP routine already does this. Therefore, you
- will make a copy of the COMPAREF.ASM source file, and delete the six lines
- between the call to B$FCMP and the Ret instruction. You can also remove
- the Exit: label if you like, although its presence causes no harm. Of
- course, the code itself is so simple that the best solution may be to
- simply duplicate the same instructions inline in your routine.
-
-
- EXPLOITING MASM'S FEATURES
- ==========================
-
- Each example I have shown so far introduced another useful MASM feature.
- For example, you learned how MASM lets you establish data memory with an
- initial value, so you don't have to assign it explicitly. But there are
- several other features you should know about as well. One is conditional
- assembly.
-
-
- CONDITIONAL ASSEMBLY
-
- With conditional assembly you can specify that only certain portions of a
- file are to be assembled. This makes it easier to maintain two different
- versions of a routine, for example one for near strings and one for far
- strings. If you had to create two separate copies of the source file, any
- improvements or bug fixes that you add would have to be done twice.
- There are two ways that a section of code can be optionally included or
- excluded. One is to define a constant at the beginning of the source file,
- and then test that constant using a form of IF and ELSE test. Like BASIC,
- MASM lets you define constant values using meaningful names. The problem
- with this method--albeit a minor one--is that you must alter the code prior
- to assembling each version. The example that follows shows how this kind
- of conditional assembly is employed.
-
- MyConst = 1
- .
- .
- IF MyConst
- ;do whatever you want here
- ELSE ;the ELSE is optional
- ;do whatever else you want here
- ENDIF
- .
- .
-
- The idea is that if you want the code that follows the IF test to be
- assembled, you would use a non-zero value for MyConst. If you wanted to
- create an alternate version using the code within the optional ELSE block,
- you would change the value to be zero.
- You can also use IFE (If Equal to zero) to test if a constant is zero.
- And this brings up another interesting MASM feature. There are actually
- two types of constants you can define. The constant MyConst shown above
- is called a *redefinable* constant, because you can actually change its
- value during the course of a program. The other type of constant is
- defined using the Equ (Equate) directive, and may not be changed:
-
- YourConst Equ 100
-
- Redefinable constants are often used in repeating macros, and macros are
- discussed later in this section.
- The other way to tell MASM that it is to assemble just a portion of the
- file is with IFDEF. IFDEF (If Defined) tests if a constant has been
- defined at all, as apposed to comparing for a specific value. The value
- of this approach is that you can define a constant on the MASM command line
- when you run it. The first example below tells MASM to assemble the code
- within the IFDEF block, and the second tells it to not to.
-
- C:\ASM\> masm program /def myconst ;
-
- C:\ASM\> masm program ;
-
- Here's the portion of the routine that is being assembled conditionally:
-
- IFDEF MyConst
- ;do something optional here
- ENDIF
-
- Likewise, IFNDEF (If Not Defined) tests if a constant has not been defined
- when reversing the logic is more sensible to you. MASM includes a great
- number of such conditional tests, and only by reading that section of the
- MASM manual will you become familiar with those that are the most useful.
-
-
- COMMENT BLOCKS
-
- Another useful MASM feature that I personally would love to see added to
- BASIC is multi-line comment blocks. The Comment command accepts any single
- character you choose as a delimiter, and considers everything thereafter
- to be comments until the same character is encountered. Many programmers
- use a vertical bar, because it is not a common character:
-
- Comment |
- This program is intended to blah blah blah, and it works
- by loading AX with blah blah blah.
- |
-
- Besides avoiding the need to place an explicit semicolon on each comment
- line, this also makes it easy to remark out large sections of code while
- you are debugging a routine.
-
-
- QUOTED STRINGS
-
- Yet another useful feature is MASM's willingness to use either single or
- double quotes to indicate ASCII text and individual characters. In BASIC,
- if you want to specify a double quote you must use CHR$(34)--it simply is
- not legal to use """, where the quote in the middle is the character being
- defined. [With the introduction of VB/DOS triple quotes may now be used
- for this purpose.] If you need to define a double quote simply surround
- it with apostrophes like this:
-
- SomeData DB '"'
- Mov AH, '"'
-
- Or you can place a single quote within double quotes like this:
-
- Add DL, "'"
-
- MASM can use either convention as needed, which is a feature I personally
- like a lot.
-
-
- LENGTH AND ADDRESS SELF-CALCULATION
-
- Whenever MASM sees the dollar sign ($) operator it interprets that to mean
- *here*, or the current address. This can be used both for data and code,
- though it is more common with data as the example below illustrates.
-
- .Data
- Descriptor DW MsgLen, Address
- Message DB "This is a message."
- Address = Offset Message
- MsgLen = $ - Address
-
- The expression $ - Address tells the assembler to take the current data
- address, and subtract from that the address where Message begins. This is
- a very powerful concept because it frees the programmer from many tedious
- calculations. In particular, if the string contents are changed at a later
- time, the new length is recalculated by MASM automatically.
-
-
- DEFINING DATA STRUCTURES
-
- To assist you in manipulating data structures, MASM offers the Struc
- directive. This is identical to BASIC's TYPE statement, whereby you define
- the organization of a collection of related data items. The example below
- shows how to define a custom data structure using BASIC, followed by an
- equivalent MASM Struc definition.
-
-
- BASIC:
-
- TYPE MyType
- LastName AS STRING * 15
- FirstName AS STRING * 12
- ZipCode AS STRING * 5
- RecordPtr AS LONG
- END TYPE
- DIM MyVar AS MyType
-
-
- MASM:
-
- Struc MyStruc
- LastName DB 15 Dup (?)
- FirstName DB 12 Dup (?)
- ZipCode DB 5 Dup (?)
- RecordPtr DD ?
- MyStruc Ends
- MyVar DB Size MyStruc Dup (?)
-
-
- Like BASIC, defining a structure merely establishes the number and type
- of data items that will be stored; memory is not actually set aside until
- you do that manually. In BASIC, you must use DIM to establish the memory
- that will hold the TYPE variable. In assembly language you instead use DB
- in conjunction with the Size directive, to set aside the appropriate number
- of bytes.
- Each component of the Structure is defined using an identifying name and
- a corresponding data type. Then, whenever a structure member is referenced
- in your assembler routine, MASM replaces it with a number that shows how
- far into the structure that member is located. MASM uses the same syntax
- as BASIC, with a period between the data name and the structure identifier.
- Here are a few examples:
-
- Mov AL,[BX+MyVar.LastName] ;same as Mov AL,[BX+15]
- Les DI,[MyVar.RecordPtr] ;loads ES:DI from RecordPtr
-
-
- MINIMIZING DGROUP USAGE
- =======================
-
- In many cases you will store the variables your routines need in DGROUP
- using the .Data directive. As with static subprograms and functions in
- BASIC, this data will not change between subroutine calls. But this also
- means that these variables are combined into the same 64k segment that is
- shared with BASIC. When there are many variables or many different
- routines each with their own variables, this can significantly reduce the
- amount of near memory available to BASIC. There are two effective
- solutions to this problem.
-
-
- LOCAL VARIABLES
-
- One way to reduce the DGROUP impact of many variables is to place some of
- them onto the system stack. MASM lets you do this automatically with its
- Local directive, or you can do it manually by subtracting the requisite
- number of bytes from SP. Of course, there is only so much room on the
- stack, so this approach is most useful when there are many routines and
- each has less than 1K or so of data. Stack variables are also useful when
- programming for OS/2 or Windows. These operating systems require that all
- of your procedures be reentrant so static variables cannot be used.
- The example below creates room for fifty words of local storage on the
- stack, and then clears the variables to zero.
-
- Routine Proc Uses ES DI, Param1:Word, Param2:Word
- Sub SP,100 ;50 words = 100 bytes
- Push SS ;assign ES from SS
- Pop ES
- Mov DI,SP ;point DI to the start of storage
- Xor AX,AX ;fill with zeros
- Mov CX,50 ;clear fifty words
- Rep Stosw ;store AX CX times at ES:[DI]
- . ;the routine continues
- .
- Add SP,100 ;restore SP to what it had been
- Ret ;return to BASIC
- Routine Endp
-
- MASM can also do this automatically for you using Local like this:
-
- Routine Proc Uses ES DI, Param1:Word, Param2:Word
- Local Buffer [100]:Byte
- Lea DI,Buffer ;clear the stack variables here
- . ;the routine continues
- .
- Ret ;return to BASIC
- Routine Endp
-
- As you can see, Local lets you refer to the start of the local stack data
- area by name. Notice how Lea is required here, because the address of
- Buffer is expressed as an offset from BP. That is, MASM translates the
- Lea instruction to Lea DI,[BP-100]. You cannot use Mov DI,Offset Buffer
- because Buffer's address (which is based on the current setting of the
- stack pointer) is not known when the routine is assembled or linked.
- In this case only one local block is defined, so you could also use Mov
- DI,SP to set DI to point to the start of the data. It is not strictly
- necessary to clear the stack space before using it, but it is important to
- understand that whatever junk happened to be in memory at that time will
- still be there after using Local.
- It is also important to be aware of a number of bugs with the Local
- directive. I have found that limiting the use of Local to a single set of
- data as shown here is safe with all MASM versions through 5.1. Using
- multiple Local directives defined with data structures can result in the
- wrong part of the stack being written to when a structure member is
- accessed by name.
-
-
- STORING DATA IN THE CODE SEGMENT
-
- Another time-honored technique for conserving DGROUP memory is to place
- selected variables into the code segment. In most cases storing data for
- a routine in the code segment will make your programs slightly larger and
- slower, because of the need for an added CS: segment override. But when
- large amounts of data must be accommodated, this can be very valuable
- indeed. One advantage to using the code segment is that you can establish
- initial values for the data, which is not possible when using the stack.
- As an example of this technique, I have written a string function called
- Message$ that stores a series of messages in the code segment. In this
- case only a single CS: segment override is needed, so the impact of using
- the code segment for data is insignificant. Message$ is designed to be
- declared and invoked as follows:
-
- DECLARE FUNCTION Message$(BYVAL MsgNumber%)
- Result$ = Message$(AnyInt%)
-
- Message$ is table driven, which makes it simple to modify the routine to
- change or add messages without having to make any changes to the function's
- structure. As shown here, Message$ is designed to return the name of a
- weekday, given a value between one and seven. You can easily modify it to
- return other strings of nearly any length.
-
- .Model Medium, Basic
- Extrn B$ASSN:Proc ;BASIC's assignment routine
-
- .Data
- Descriptor DD 0 ;the output string descriptor
- Null$ DD 0 ;use this to return a null
- ; (needed for BASIC PDS only,
- .Code ; but okay with QuickBASIC)
-
- Message Proc Uses SI, MsgNumber:Word
-
- Mov SI,Offset Messages ;point to start of messages
- Xor AX,AX ;assume an invalid value
-
- Mov CX,MsgNumber ;load the message number
- Cmp CX,NumMsg ;does this message exist?
- Ja Null ;no, return a null string
- Jcxz Null ;ditto if they pass a zero
-
- Do: ;walk through the messages
- Lods Word Ptr CS:0 ;load and skip over this message's length
- Dec CX ;show that we read another
- Jz Done ;this is the one we want
-
- Add SI,AX ;skip over the message text
- Jmp Short Do ;continue until we're there
-
- Done:
- Or AX,AX ;are we returning a null?
- Jz Null ;yes, handle that differently
- Push CS ;no, pass the source segment
-
- Done2:
- Push SI ;and the source address
- Push AX ;and the source length
-
- Push DS ;pass the destination segment
- Mov AX,Offset Descriptor ;and the destination address
- Push AX
- Xor AX,AX ;0 means assign a descriptor
- Push AX ;pass that as well
-
- Call B$ASSN ;let B$ASSN do the dirty work
- Mov AX,Offset Descriptor ;show where the output is
- Ret ;return to BASIC
-
- Null:
- Push DS ;pass the address of Null$
- Mov SI,Offset Null$
- Jmp Short Done2
-
- Message Endp
-
-
- ;----- DefMsg macro that defines messages
- DefMsg Macro Message
- LOCAL MsgStart, MsgEnd ;;local address labels
- NumMsg = NumMsg + 1 ;;show we made another one
- IFB <Message> ;;if no text is defined
- DW 0 ;;just create an empty zero
- ELSE ;;else create the message
- DW MsgEnd - MsgStart ;;first write the length
- MsgStart: ;;identify the starting address
- DB Message ;;define the message text
- MsgEnd Label Byte ;;this marks the end
- ENDIF
- Endm
-
-
- Messages Label Byte ;the messages begin here
- NumMsg = 0 ;tracks number of messages
- ;DO NOT MOVE this constant
- DefMsg "Sunday"
- DefMsg "Monday"
- DefMsg "Tuesday"
- DefMsg "Wednesday"
- DefMsg "Thursday"
- DefMsg "Friday"
- DefMsg "Saturday"
- End
-
- After declaring BASIC's B$ASSN routine as being external, Message$ defines
- two string descriptors in the Data segment. The first is used for the
- function output when returning a normal message, and the second is used
- only when returning a null string. In truth, the need for a separate
- output descriptor and the slight added steps to detect the special case of
- a null output string is needed only with BASIC PDS far strings. And this
- brings up an important point.
- It is impossible to write one assembly language subroutine that can work
- with both QuickBASIC and BASIC PDS far strings using the normal, documented
- methods. To create a string function for use with QuickBASIC and PDS near
- strings, you define and fill in a string descriptor in DGROUP, and assign
- its address in AX before returning to BASIC. And to return a far string
- as a function for PDS requires calling the internal STRINGASSIGN routine
- that Microsoft provides with PDS. STRINGASSIGN works with both near and
- far strings in PDS, but is not available in QuickBASIC.
- The trick is to use the *undocumented* name B$ASSN, which is really the
- same thing as STRINGASSIGN. The big difference, though, is that B$ASSN is
- available in all versions of BASIC 4.0 and later. When near strings are
- used the B$ASSN routine is extracted from the near strings library. When
- linking with far strings a different version is used, extracted by LINK
- from the far strings library. This is a powerful concept to be sure, and
- one we will use again for other examples later on in this chapter.
- Message$ begins by loading SI with the starting address of a table of
- messages. These messages are located at the end of the source file in the
- code segment, and each is preceded with the length of the text. Although
- it may not be obvious from looking at the source listing, the message data
- is actually structured like this:
-
- DW 6
- DB "Sunday"
- DW 6
- DB "Monday"
- .
- .
-
- Next, AX is cleared to zero just in case the incoming string number is
- illegal. Later in the program AX holds the length of the output string;
- clearing it here simply makes the program's logic more direct.
- CX is then loaded with the message number the caller asked for. If CX
- is either higher than the available number of messages or zero, the program
- jumps to the code that returns a null string. Otherwise, a small loop is
- entered that walks through each message, decrementing CX as it goes. When
- CX reaches zero, SI is pointing at the correct message and AX is holding
- its length. Otherwise, the current length is added to SI, thus skipping
- over that data.
- Notice the unusual form of the Lodsw statement, to allow it to work with
- a CS: override. MASM has a number of quirks that are less than intuitive,
- and this is but one of them. Normally you would use either Lodsb or Lodsw,
- to indicate loading either a byte into AL or a word into AX. But when you
- use a segment override MASM requires omitting the "b" or "w" Lods suffix,
- and you must state Byte Ptr or Word Ptr explicitly. Then, a dummy argument
- must be placed after the override colon.
-
-
- MASM MACROS
-
- The last new feature this listing introduces is the use of macros. The
- most basic use of MASM macros is to define a block of code once, and then
- repeat it multiple times with a single statement. This is not unlike
- keyboard macro programs such as Borland's SuperKey, that let you assign a
- string of text to a single key. For example, you could press Alt-S and
- SuperKey will type "Very truly yours", five Enter keys, and then your name.
- MASM macros also offer many other interesting and useful capabilities,
- including the ability to accept arguments. [I should mention that the main
- point of the DefMsg macro is to make this function easy to modify, so you
- can create other, similar string functions from this same routine.] Before
- attempting to explain the DefMsg (Define Message) macro I designed for use
- with Message$, let's consider some macro basics.
- Say, for example, you find that a particular routine needs to push the
- same five registers many times during the course of a procedure. To
- simplify this task you could define a macro--perhaps named PushRegs--that
- performs the code sequence for you. Such a macro definition would look
- like this:
-
- PushRegs Macro
- Push AX
- Push BX
- Push SI
- Push DS
- Push ES
- PushRegs Endm
-
- Now, each time you want to execute this series of instructions you would
- simply use the command PushRegs. Please understand that a macro is not the
- same as a called subroutine. The assembler still places each Push command
- in sequence into your source code each time the macro is invoked. But a
- simple macro like this can reduce the amount of typing you must do, and
- minimize errors such as pushing registers in the wrong order. And in some
- cases Macros also make your code easier to read.
- As I mentioned, a MASM macro can accept arguments, and it can even be
- designed to accept a varying number of them. If you need to push three
- registers but which ones may change, you would define PushRegs like this:
-
- PushRegs Macro Reg1, Reg2, Reg3
- Push Reg1
- Push Reg2
- Push Reg3
- Endm
-
- Then to push AX, SI, and DI you would invoke PushRegs as follows:
-
- PushRegs AX, SI, DI
-
- Of course, a corresponding PopRegs macro would be defined similarly. Once
- a macro has been defined you can pass any legal argument to it. For
- example, you could also use this:
-
- PushRegs AX, Word Ptr [BP-20], IntVar
-
- Here, you are pushing AX, the word 20 bytes below where BP points to on
- the stack, and the integer variable named IntVar.
- A useful enhancement to this macro would let you pass it a varying
- number of parameters. The PushM macro that follows accepts any number of
- arguments (up to eight), and pushes each in sequence.
-
- PushM Macro A,B,C,D,E,F,G,H ;;add more place-holders to suit
- IRP CurArg, <A,B,C,D,E,F,G,H> ;;repeat for each argument
- IFNB <CurArg> ;;if this arg is not blank
- Push CurArg ;;push it
- ENDIF
- Endm ;;end of repeat block
- Endm ;;end of this macro
-
- From this you can create a complementary PopM macro by changing the name,
- and also changing the Push instruction to Pop.
- The IRP command works much like a FOR/NEXT loop in BASIC, and tells MASM
- to repeat the following statements for each argument that was given. IFNB
- (If Not Blank) then tests each argument to see if it was in fact present
- in the incoming list of parameters. In this case, CurArg assumes the name
- of the argument, and the Push instruction is expanded to specify that name.
- There is no disputing that the syntax of a MASM macro is confusing at
- best. Having to enclose some arguments in angle brackets but not others
- requires frequent visits to the MASM manual. Further, a MASM macro is
- virtually impossible to debug. If you write a macro incorrectly or create
- a syntax error, MASM reports an error at the line where the macro was
- invoked, rather than at the line containing the error in the macro. It is
- not uncommon to receive a number of errors all pointing to the same source
- line, with no indication whatsoever where the error really is.
- Now consider how the DefMsg macro operates. DefMsg begins by defining
- a single incoming parameter named Message. Two local labels--MsgStart and
- MsgEnd--are defined, and these are needed so MASM can calculate the length
- of the messages. Although labels within a macro do not have to be declared
- as local, you would get an error if the macro were used more than once.
- Like BASIC, the assembler requires that each label have a unique name. By
- using local labels MASM generates a new, unique internal name for each
- macro invocation, instead of the actual label name given.
- The next statement increments a MASM variable named NumMsg. To avoid
- an error caused by calling Message$ with an invalid message number, it
- compares the number you pass to the number of messages that are defined.
- This test occurs in the fourth line of the procedure, at the Cmp CX,NumMsg
- statement. NumMsg is a constant, except it may be redefined within the
- routine. (When a constant is assigned using the word Equate, its value
- may not be changed by either your source code or by a macro.) But when a
- variable is defined using an equals sign (=), MASM allows it to be altered
- as it assembles your program. Understand that the resulting number is
- added to your program as a constant. However, its value can be changed
- during the course of assembly. Therefore, each time DefMsg is invoked, it
- increments NumMsg. MASM places the final value into the Cmp instruction,
- as if you had defined it using a fixed known value.
- The IFB (If Blank) test checks to see if DefMsg was given a parameter
- when it was invoked. In most cases you will probably want to define a
- series of consecutive messages. As it is used here, seven different day
- names are returned in sequence. But there may be times when you want to
- leave a particular message number blank. For example, you could create a
- series of messages that correspond to BASIC's error numbers. BASIC file
- error numbers range from 50 through 76, but there are no messages numbers
- 60, 65, or 66. You could therefore leave those blank, and invoke a
- modified copy of Message$ like this:
-
- CALL DOSMessage$(51 - ERR)
-
- When DefMsg is used with no argument, it merely creates a zero word at
- that point in the code segment. Otherwise, the length of the message is
- stored, followed by the message text. The statement DW MsgEnd - MsgStart
- is replaced with the difference between the addresses, which MASM
- calculates for you. This is similar to the earlier example that showed how
- a dollar sign ($) can simplify defining strings that may change.
- The last macro I will describe here is Rept, which means "Repeat the
- following statements a given number of times". In the simplest sense, Rept
- could be used to generate a series of the same instructions:
-
- Rept 100
- Xor AX,AX
- Push AX
- Call SomeProc
- Endm
-
- A Rept macro is not invoked by name; rather, it is added inline to a
- program (or included within a macro that is called by name). In most cases
- you would use a coding loop to repeat a block of code, since a Rept macro
- actually generates the same code repeatedly in the program. But there are
- situations where timing is very critical, and a loop is always somewhat
- slower than a sequence of inline instructions.
- Another good use for Rept is in conjunction with redefinable equates,
- such as this example which defines the letters of the alphabet:
-
- Alphabet:
- Char = 0
- Rept 26 ;;do this 26 times
- DB "A" + Char ;;define ASC("A") + Char
- Char = Char + 1 ;;increment Char
- Endm
-
- Although the MASM manual states that you must use double semicolons for
- remarks within a macro as shown here, I have used a single semicolon
- without problems.
- There are other macro commands and features I will not describe here,
- because I have not found them to be particularly useful. However, macros
- can be recursive, multiple macros may be nested, and even redefined on the
- fly. I urge you to refer to the documentation that Microsoft provides for
- more information on those advanced features.
-
-
- SEGMENT NAMING
- ==============
-
- Aside from the short PrtSc example shown earlier in this chapter, we have
- relied upon MASM's simplified segmentation directives to spare us from the
- nuisance of defining and naming segments. Indeed, when writing routines
- that will be added to BASIC it is rarely necessary to do this manually, so
- why bother?
- One place where naming segments explicitly is useful is when you have
- many internal procedures that are never called from BASIC directly. If,
- for clarity and organization reasons, you decide to store those routines
- in different files, you still may want to access the routines using near
- calls. Since a near call is two bytes shorter than a far call and also
- operates slightly faster, this can make a difference when there are many
- Call commands within the routines.
- As LINK pulls all of the various pieces of your program together from
- separate object and library files, it reads the segment names and combines
- those with the same name. Thus, a routine in one source file can call a
- routine in a different file, and LINK will place both routines into the
- same segment if they use the same segment name. This is of course needed
- to ensure that the called routine is reachable by the caller (within 64K).
- All of the standard segment names that Microsoft recommends are listed
- in the MASM manual, along with instructions for creating your own names.
- Therefore, I won't belabor that here.
-
-
- ACCESSING BASIC INTERNALS
- =========================
-
- In preceding sections you learned that it is possible--even desireable--
- to call BASIC's internally routines directly. Besides those that have
- already been described, there are several other useful routines that can
- be accessed from assembly language. One of these is B_ONEXIT, which lets
- you tap into BASIC's termination procedure.
- When a BASIC program ends by running out of statements, or by using END,
- STOP, or SYSTEM, BASIC makes a call to a central routine that in turn tells
- DOS to end the program. If a fatal error occurs and there is no ON ERROR
- handler, BASIC also calls a routine that prints an error message. B_ONEXIT
- lets you tell BASIC the segment and address of a routine you want called
- as part of the termination process. B_ONEXIT is supported only in
- QuickBASIC version 4.5 and BASIC PDS.
- One reason you might want to use B_ONEXIT is to ensure that interrupts
- taken over by your assembler routine are restored properly. Taking over
- interrupts will be described later in the section "Handling Interrupts."
- Here's a program fragment showing how B_ONEXIT is set up and called:
-
-
- Extrn B_ONEXIT:Proc ;declare B_ONEXIT as external
- Push CS ;pass your code segment
- Mov AX,Offset TermProc ;and the address of the routine
- Push AX ; that is to be called
- Call B_ONEXIT ;register it with B_ONEXIT
- .
- .
-
- TermProc Proc ;this is the routine to be called
- . ;do whatever you need to here
- .
- Ret ;don't forget to return!
- TermProc Endp
-
-
- BASIC's INTERNAL DATA
-
- There are two internal variables BASIC maintains that you will find useful.
- One is the current DEF SEG setting, and it is stored in the integer
- variable named B$SEG. The other is the current color value that is used
- by PRINT and CLS. The foreground and background colors are stored combined
- in a single word named B$FBColors. The reason these are useful is because
- you may want to change and then restore them from inside a BASIC
- subprogram. Much of the benefit of reusable programming is lost if you
- cannot put things back to the way they were originally.
- For example, if you have written a BASIC routine that prints an error
- message in bright red at the bottom of the screen, you will need to use a
- subsequent COLOR command to put the color back to what it had been. But
- what color do you use? The same holds true for a routine that changes the
- current DEF SEG setting, perhaps before loading or saving a file using
- BLOAD or BSAVE. If you cannot return that to its original value, extra
- work is needed in the main program each time the routine is used.
- Access to B$SEG requires a single assembler instruction, as shown in the
- complete GetSeg function shown following. Declare and use GetSeg like
- this:
-
- DECLARE FUNCTION GetSeg%()
- SavedSeg = GetSeg%
- .
- .
- DEF SEG = SavedSeg
-
-
- ;GETSEG.ASM
- .Model Medium, Basic
- .Data
- Extrn B$Seg:Word
-
- .Code
- GetSeg Proc
-
- Mov AX,B$Seg ;load the value from B$Seg
- Ret ;return with the function output in AX
-
- GetSeg Endp
- End
-
-
- Because BASIC combines its colors into a single word, a few extra steps
- are needed to separate them. Call GetColor like this:
-
- CALL GetColor(FG%, BG%)
-
- FG% and BG% are returned to you holding the current foreground and
- background color values. Here's how GetColor works:
-
-
- ;GETCOLOR.ASM
- .Model Medium, Basic
- .Data
- Extrn B$FBColors:Word
-
- .Code
-
- GetColor Proc, FG:Word, BG:Word
-
- Mov DX,B$FBColors ;load the combined colors
- Mov AL,DL ;copy the foreground portion
- Cbw ;convert it to a full word
- Mov BX,FG ;get the address for FG%
- Mov [BX],AX ;assign FG%
- Mov AL,DH ;load the background portion
- Mov BX,BG ;get the address for BG%
- Mov [BX],AX ;assign BG%
- Ret ;return to BASIC
-
- GetColor Endp
- End
-
-
- One unfortunate problem is that GetColor cannot be used in the editing
- environment. When BASIC compiles a PEEK or POKE statement, it generates
- inline code that loads ES with the segment from B$SEG, and then reads or
- writes the data at the specified address. Therefore, the current segment
- must be available to BASIC routines that use PEEK or POKE in a Quick
- Library. But the color values are accessed only by routines in BASIC's
- runtime library, so the information is not made available to procedures in
- a Quick Library. Because of this issue, the GetColors routine is provided
- on the accompanying disk only in the BASIC.LIB and BASIC7.LIB linking
- libraries.
- There are several other internal data items you may want to know about,
- and one that I have found useful is called __osversion. This byte holds
- the major DOS version number; for example, if DOS 3.x is running then
- __osversion will hold the value 3. Even though it is trivial to query DOS
- for the number, why bother since you can get it this way with a single Mov.
-
-
- BASIC's INTERNAL ROUTINES
-
- Besides the procedures and internal data I have described previously, there
- are many others you will no doubt find useful. You can, for example, call
- SETMEM prior to claiming memory from DOS. And although the B$ASSN routine
- can assign any type of data from any other type including strings, a
- simplified version is also present to assign to and from conventional
- strings only.
- As you have seen, the beauty of using BASIC's own routines is that
- identical code can be used for both near and far strings. In either case,
- the string descriptors are known to reside in DGROUP, and the internal
- routines are designed to operate on those descriptors. You don't even have
- to know which of the string libraries (near or far) is being used.
- There are also several math routines that can be accessed directly,
- including those that multiply, divide, and compare long integers. Even if
- you know how to do that, it's always easier to call BASIC's routines. This
- result in less code as well. And if you need to read the current cursor
- position, you can access CSRLIN and POS(0) directly. In some cases, you
- can't read that information from the BIOS, so calling BASIC is the only
- reliable way to get it.
- The following section documents the BASIC internal routines that I have
- found useful when called from assembly language. I have purposely omitted
- routines that handle BASIC commands such as PRINT, INKEY, GET, and PUT.
- Even though several of these were described throughout the course of this
- book, they have little relevance within a called assembler routine.
- BASIC's internal services that follow are listed in alphabetical order,
- based on their call names. Be sure to declare them as external procedures
- in your routine's source code.
-
-
- B$CPI4: Compare Two Long Integers
-
- B$CPI4 expects two long integer arguments to be placed onto the stack by
- value, and it returns the result of its comparison in the Flags register.
- For example, to see if Var1 is greater than Var2 you'd use code like this:
-
- Push Word Ptr [Var1+2] ;first push Var1's high word
- Push Word Ptr [Var1] ;and then its low word
- Push Word Ptr [Var2+2] ;next do the same for Var2
- Push Word Ptr [Var2]
- Call B$CPI4 ;compare them
- Jg Label ;Var1 is indeed greater
-
- Remember that long integers are compared by BASIC on a signed basis, so
- you should use Jg or Jl rather than Ja or Jb. The letters CPI4 stand for
- Compare Integer 4 bytes.
-
-
- B$CSRL: CSRLIN Function
-
- B$CSRL is called with no arguments, and it returns BASIC's current row in
- AX as follows:
-
- Call B$CSRL
- . ;do what you want with AX
-
-
- B$DVI4: Divide Two Long Integers
-
- Like B$CPI4, B$DVI4 (Divide Integer 4 bytes) expects the incoming integer
- arguments to be passed by value on the stack. The result is then returned
- in DX:AX as a long integer:
-
- Push Word Ptr [Var2+2] ;always push the high word first
- Push Word Ptr [Var2] ;then the low word
- Push Word Ptr [Var1+2] ;ditto for Var2
- Push Word Ptr [Var1]
- Call B$DVI4 ;divide them
- . ;now DX:AX holds Var1 \ Var2
-
- Notice that with B$DVI4, the divisor is pushed first onto the stack,
- followed by the dividend.
-
-
- B$FPOS: POS(0) Function
-
- Even though the argument passed to BASIC's POS(0) is ignored, it is still
- expected mainly for historical reasons. Therefore, you must push
- something--anything--onto the stack before calling B$FPOS:
-
- Push AX
- Call B$FPOS
- . ;now AX holds the column
-
-
- As with all of BASIC's functions that return an integer, B$FPOS returns
- the current column in AX. The leading F in FPOS stands for Function.
-
-
- B$FRI2: FRE() Function
-
- B$FRI2 (Free Integer 2 bytes) requires an incoming integer argument by
- value on the stack, and for safety you should use this for the -1 and -2
- variations only.
- Using -1 reports the total amount of memory that is available to BASIC,
- so you might use this before calling SETMEM to release memory for your own
- uses. Although B$FRI2 uses an integer for an argument, it returns a long
- integer in DX:AX. You can also use an argument of -2 to see how much stack
- space is available:
-
- Mov AX,-2
- Push AX
- Call B$FRI2
- . ;now DX:AX holds the available stack space
-
-
- B$RDIM: REDIM Statement
-
- In most cases you will probably not find the ability to call REDIM directly
- very valuable. One notable exception is explained later in the section
- entitled "Reading the Array Descriptor," where I show how to size and then
- load a string array with all of the files that match a given search
- specification.
- B$RDIM is fairly complicated to set up and call, because it accepts a
- varying number of parameters. This is needed because BASIC accepts a
- variable number of dimensions, and the same routine is used for all cases.
- The following example shows how to prepare and call this routine when
- resizing a one-dimensional array.
-
- Mov AX,LBound ;first pass the lower bound value
- Push AX
- Mov AX,UBound ;then pass the upper bound
- Push AX
- Mov AX,ElementLength ;next the length of each element
- Push AX
- Mov AX,Features ;see the accompanying text for
- Push AX ; information on these two items
- Mov AX,Offset ArrayDescriptor
- Push AX
- Call B$RDIM ;call REDIM to do it
-
- Chapter 2 described the array descriptor in detail, including the Features
- word. However, you must not use REDIM to create a new array where none
- existed before. Instead, you will read the current features from the
- existing array descriptor, and pass the same values on again to B$RDIM.
- This will be shown in context momentarily.
-
-
- B$STDL: String Delete
-
- You can call B$STDL to delete a string or string array element, and it
- requires less code than assigning the string from another, null string.
- The single argument is the address of a string descriptor:
-
- Mov AX,Offset Descriptor
- Push AX
- CALL B$STDL
-
-
- B$SETM: SETMEM Function
-
- B$SETM expects a long integer argument by value on the stack; if the value
- is negative then that much memory is released back to DOS, and thus taken
- from your BASIC program. However, you should call B$SETM again later with
- a positive value when you are finished, so the BASIC program can reclaim
- that memory. Since SETMEM is a function, B$SETM also returns the amount
- of memory currently available in the DX:AX register pair.
-
-
- B$SASS: String Assign
-
- Where B$ASSN is capable of assigning any mix of conventional and fixed-
- length strings, B$SASS works with conventional strings only. However, it
- requires only two parameters instead of six:
-
- Mov AX,Offset Source$
- Push AX
- Mov AX,Offset Destination$
- Push AX
- CALL B$SASS
-
- Note that if the destination string is not null, its current contents are
- released after assigning it from Source$. This is the normal way that
- strings are assigned, and B$ASSN also works like this.
-
-
- Finding Other Routines
-
- The routines just described are those that I personally have found to be
- useful. Discovering other routine names and how they are called is in
- fact quite simple. If you wanted to access, say, COMMAND$, you would write
- a one-line BASIC program, and then examine the code that is generated using
- Microsoft CodeView. CodeView lets you see which and how many parameters
- are being passed as well as the routine name being called, making
- exploration both easy and fun.
- BASIC string functions such as COMMAND$ and ENVIRON$ return the DGROUP
- address of the result string descriptor in AX, just like an assembly
- language function you would write. If you do call a built-in BASIC
- function, be sure to also pass its output descriptor to B$STDL (String
- Delete) when you are done with it. Otherwise, the string space it uses
- [and the temporary output descriptor] will never be released.
-
-
- READING THE ARRAY DESCRIPTOR
-
- Chapter 2 described the BASIC array descriptor in detail, and discussed
- each of the components it contains. Understanding how an array descriptor
- works opens many opportunities to assembly language programmers, because
- it lets you write routines that accept an array passed with empty
- parentheses. This was shown in the Sort routine introduced in Chapter 8,
- although the techniques used there were not detailed.
- As an example of the possibilities direct access to an array descriptor
- offers, I will show a subroutine that accepts a file specification, and
- returns a string array filled with the names of all matching files.
- GetNames calls upon three internal BASIC routines: B$FLEN, B$RDIM, and
- B$ASSN. B$FLEN returns the length of a string, and is used here to know
- how long the file specification is. B$RDIM redimensions the passed string
- array to the correct number of elements, based on the number of matching
- file names that are found. B$ASSN then assigns each element to those
- names.
- This next short BASIC program shows how GetNames is set up and used.
-
-
- DECLARE FUNCTION GetNames%(Array$())
- REDIM Array$(1 TO 1) 'use REDIM, not DIM
- Array$(1) = "*.*" 'any valid spec is okay
- NumFiles% = GetNames%(Array$()) 'load all names at once
-
- IF NumFiles% = 0 THEN 'were any files found?
- PRINT "No matching files." 'no, say so and end
- END
- END IF
-
- FOR X% = 1 TO NumFiles% 'yes, print each name
- PRINT Array$(X%)
- NEXT
- PRINT NumFiles; "matching files were found"
-
-
- As you can see, you must establish the array initially using REDIM. To
- avoid the need for an extra parameter, the file specification is passed in
- the first element of the array. Furthermore, GetNames returns the number
- of files that matched as an integer result. If no files were encountered,
- GetNames leaves the array as it was.
- When GetNames is called, the array may already contain other data, and
- it can have any legal upper and lower bounds. As long as the lowest
- element number contains a valid search specification, the spec can be found
- and the array will be redimensioned starting at element number one. The
- GETNAMES.BAS demonstration program on the accompanying disk adds to this
- short example by sorting the names after they are read.
- A complete description of how GetNames works follows this source
- listing.
-
- ;GETNAMES.ASM, loads a group of file names into an array
-
- .Model Medium, Basic
- Extrn B$RDIM:Proc ;this redimensions an array
- Extrn B$ASSN:Proc ;this assigns a string
- Extrn B$FLEN:Proc ;this returns a string's length
-
- DTAType Struc ;define the DOS DTA structure
- Intern DB 21 Dup (?) ;this is used by DOS internally
- FAttr DB ? ;this holds the file attribute
- FTime DW ? ;this holds the file time
- FDate DW ? ;this holds the file date
- FSize DD ? ;this holds the file size
- FName DB 13 Dup (?) ;this holds each file name
- DTAType Ends
-
- .Data
- DTA DB Size DTAType Dup (?) ;DOS places file info here
- NumFiles DW 0 ;how many names were read
- SpecLength DW 0 ;remembers file spec length
-
- .Code
-
- GetNames Proc Uses SI DI, Array:Word
-
- Local Buffer[80]:Byte ;copy the spec here, add a zero
-
- ;-- Create a local Disk Transfer Area for our own use.
- Lea DX,DTA ;show DOS where the new DTA goes
- Mov AH,1Ah ;set DTA service
- Int 21h ;call DOS to do it
-
- ;-- Read the array descriptor, get the search spec from the first element,
- ; then copy it to the stack appending a CHR$(0) byte (ASCIIZ string).
- Mov SI,Array ;get address of array descriptor
- Mov BX,[SI+0Ah] ;now BX holds adjusted offset
- Mov AX,4 ;each element is four bytes long
- Mul Word Ptr [SI+10h] ;multiply by first element number
- Add BX,AX ;BX holds first element's address
-
- Push DS ;push source segment and address
- Push BX ; for call to B$ASSN later on
- Xor AX,AX ;tell B$ASSN source is descriptor
- Push AX ;using a value of zero
-
- Push BX ;pass descriptor addr to B$FLEN
- Call B$FLEN ;this returns the length in AX
- Mov SpecLength,AX ;save length locally for a moment
-
- Lea AX,Buffer ;get the destination address
- Push SS ;pass the segment to assign into
- Push AX ;and then the address
- Push SpecLength ;we're assigning a fixed length
- Call B$ASSN ;copy the file spec to the stack
-
- Lea BX,Buffer ;retrieve start address of spec
- Mov DX,BX ;copy to DX where DOS expects it
- Add BX,SpecLength ;point just past end of string
- Mov Byte Ptr [BX],0 ;and append trailing zero byte
-
- ;-- Count the number of names that match the search specification.
- Mov AH,4Eh ;specify Find First matching name
- Mov CX,00100111b ;this matches any type of file
- Xor BX,BX ;BX counts the number of names
-
- CountNames:
- Int 21h ;see if there's a matching name
- Jc DoneCount ;carry set means no more names
- Inc BX ;otherwise, we found another one
- Mov AH,4Fh ;find the next matching name
- Jmp CountNames ;continue until there are no more
-
- DoneCount:
- Mov NumFiles,BX ;remember how many files we found
- Or BX,BX ;did we fail on the first name?
- Jz Exit ;yes, return a count of zero
-
- ;-- Now that we know how many file names there are, REDIM the string array.
- Mov AX,1 ;specify an LBOUND of 1
- Push AX ;pass that on to B$RDIM
- Push BX ;and pass on the new UBOUND value
- Mov AL,4 ;each descriptor takes four bytes
- Push AX ;pass that on too
-
- Mov BX,Array ;get array descriptor again
- Mov AX,[BX+08] ;load the existing Features word
- Push AX ;use that again for this call
- Push BX ;show where array descriptor is
- Call B$RDIM ;finally, redimension the array
-
- ;-- This is the main processing loop that reads and assigns each name
- ; that is found.
- Mov AH,4Eh ;specify Find First matching name
- Lea DX,Buffer ;load address of file spec again
- Mov BX,Array ;get array descriptor address too
- Mov BX,[BX+0Ah] ;reload the adjusted offset value
- Add BX,4 ;BX is first descriptor address
-
- Do:
- Mov CX,00100111b ;specify any type of file again
- Int 21h ;see if there's a matching name
- Jc Exit ;carry set means no more names
- Push BX ;otherwise, save the address
-
- ;-- Search for the zero that marks the end of this name.
- Mov DI,Offset DTA.FName
- Push DS ;in anticipation of call below
- Push DI ;DI too while the address handy
-
- Push DS ;ensure that ES=DS
- Pop ES
- Mov CL,13 ;search up to 13 characters
- Repne Scasb ;do the search
- Mov AL,CL ;save the remainder in AL
-
- Mov CL,13 ;calc number of chars to copy
- Sub CL,AL ;the answer is now in CX
- Dec CX ;don't include the zero byte
- Push CX ;pass that on to B$ASSN
-
- Push DS ;show where destination string is
- Push BX
- Xor AX,AX ;zero means B$ASSN is assigning
- Push AX ; to a conventional string
- Call B$ASSN ;assign this element to the name
-
- Pop BX ;retrieve the descriptor address
- Add BX,4 ;point to the next element
- Mov AH,4Fh ;specify Find Next matching name
- Jmp Do ;and keep on keepin' on
-
- Exit:
- Mov AX,NumFiles ;assign the function output
- Ret ;return to BASIC
-
- GetNames Endp
- End
-
- GetNames begins by declaring the three BASIC routines it will call as being
- external. Next the DTA structure is defined, to simplify access to the
- file name address when it assigns each element in the string array. The
- only data items are the DTA itself, two working variables, and the local
- stack buffer. Since the incoming file specification needs to be converted
- to an ASCIIZ string for DOS, GetNames copies that specification into Buffer
- and then appends a CHR$(0) zero byte to the end.
- Once the DTA has been established, the next step is to read the file
- specification passed in the first element, and copy it into local storage.
- B$FLEN is used to obtain the length of the string, so GetNames will know
- how far into the buffer the zero byte will be placed. The last preparatory
- steps call B$ASSN telling it to copy from a conventional string (the array
- element) to a fixed-length string (Buffer), and then store the zero byte.
- The actual body of the program is broken into two portions. The first
- simply calls DOS repeatedly to count the file names, to know how many
- elements are needed. The count is then saved in NumFiles; if none were
- found GetNames exits without doing anything else. Otherwise, the incoming
- string array is redimensioned from 1 to the number of files.
- The second portion again reads each file name through DOS, but this time
- the names are actually assigned to the array elements using B$ASSN. This
- time, however, B$ASSN assigns a conventional string from the fixed-length
- string portion of the DTA. Since the source is now of a fixed-length,
- GetNames needs to know how long each name is. The longest possible name
- is 13 bytes long (eight for the name, a period, three for an extension, and
- one more for the terminating zero byte). Therefore, ES:DI is set to point
- to the start of the DTA, AX is set to zero to search for the zero byte, and
- CX is loaded with the number of characters to scan.
- Once the zero is found--and it always will be--the count that remains
- in CX is subtracted from 13 to obtain the actual length of the current
- name. Because that calculation includes the unwanted CHR$(0), CX is
- decremented by one.
- There is one small related trick that bears explaining. Just before the
- call to B$RDIM, AX is loaded with the number 1, to specify that as the
- first element number. This three-byte instruction sets AL to 1, and clears
- AH to 0. Three lines below that only AL is assigned, which is sufficient
- because we know that AH is already zero. Because the number being assigned
- is one byte long, assigning AL requires only two bytes.
- Admittedly, the savings is small, but the affect on code readability is
- minimal once you know about such tricks. And a byte saved is always
- welcome in assembly language programming. The same trick is used when
- setting CL to 13, where CH is known to be zero after assigning the file
- attribute of 00100111b to all of CX.
-
-
- HANDLING INTERRUPTS
- ===================
-
- The last programming technique I want to describe is writing an interrupt
- handler you can attach to a BASIC program. There are several applications
- for this, such as tapping into the timer interrupt to display an on-screen
- clock. Instead of having to constantly print TIME$ during your INKEY$
- input loops, such a routine would act as a sort of TSR, getting control at
- each timer tick and displaying the time automatically.
- The example I will show here takes over the keyboard interrupt, and
- disables the Ctrl-Alt-Del key sequence. This lets you prevent rebooting
- with its corresponding loss of data, should someone press those keys
- inadvertently (or on purpose!). NoReboot is called as follows:
-
- CALL NoReboot(BYVAL InstallFlag%)
-
- If InstallFlag is non-zero, you are telling NoReboot to install itself and
- take over the keyboard interrupt to prevent rebooting. An argument of
- zero instead unhooks the interrupt, and re-enables those keys. Although
- you could certainly modify NoReboot to use BASIC's B_ONEXIT service to
- deinstall itself automatically, I have left that feature out on purpose in
- the interest of clarity. This also lets you activate NoReboot selectively
- in your program, since there is no way to revoke a request to B_ONEXIT.
-
- ;NOREBOOT.ASM, traps Ctrl-Alt-Del within a BASIC program
-
- .Model Medium, Basic
- .Code
-
- NoReboot Proc Uses DS, InstallFlag:Word
-
- Cmp InstallFlag,0 ;are they asking to install?
- Je Deinstall ;no, so deinstall it
-
- Cmp CS:Old9Seg,0 ;yes, are we already installed?
- Jne Exit ;yes, and don't do that again!
-
- Mov AX,3509h ;ask DOS for current Int 9 vector
- Int 21h ;DOS returns it in ES:BX
- Mov CS:Old9Adr,BX ;save it locally
- Mov CS:Old9Seg,ES
-
- Mov AX,2509h ;point Int 9 to our own handler
- Mov DX,Offset NewInt9
- Push CS ;copy CS into DS
- Pop DS
- Int 21h
-
- Exit:
- Ret ;return to BASIC
-
-
- ;-- Control comes here when a key is pressed or released.
- NewInt9:
- Sti ;enable further interrupts
- Push AX ;save the registers we're using
- Push DS
-
- In AL,60h ;read the keyboard scan code
- Cmp AL,83 ;is it the Delete key?
- Jnz Continue ;no, continue on to the BIOS
-
- Xor AX,AX ;see if Alt and Ctrl are pressed
- Mov DS,AX ;by looking at address 0:417h
-
- Mov AL,DS:[417h] ;get shift status at 0000:0417h
- Test AL,8 ;is Alt key depressed?
- Jz Continue ;no, continue on to the BIOS
- Test AL,4 ;is Ctrl key depressed?
- Jz Continue ;no, continue on to the BIOS
-
- In AL,61h ;send an acknowledge to keyboard
- Mov AH,AL ;otherwise the Ctrl-Alt-Del
- Or AL,80h ; keystroke will still be
- Out 61h,AL ; hanging around the next time
- Mov AL,AH ; a program asks for a key
- Out 61h,AL
- Mov AL,20h ;indicate end of interrupt to the
- Out 20h,AL ; 8259 interrupt controller chip
-
- Pop DS ;ignore, simply return to caller
- Pop AX
- Iret ;use this special Ret when
- ; returning from an interrupt
- Continue:
- Pop DS ;restore the saved registers
- Pop AX
- Jmp DWord Ptr CS:Old9Adr ;continue on to the BIOS by
- ; jumping to the address
- ; that was saved during
- ; initialization
- DeInstall:
- Mov AX,2509h ;restore original Int 9 handler
- Mov DX,CS:Old9Adr ;from segment and address saved
- Mov DS,CS:Old9Seg ; earlier
- Int 21h ;DOS does this for us
- Mov CS:Old9Seg,0 ;clear this as an installed flag
- Jmp Short Exit ;and then exit back to BASIC
-
- NoReboot Endp
-
- Old9Adr DW 0 ;remembers original Int 9 address
- Old9Seg DW 0 ;these must be stored in the code
- ; segment because DS is undefined
- ; when NewInt9 receives control
- End
-
- The first thing NoReboot does is look to see if the caller is installing
- or deinstalling. If installation is requested, the saved Interrupt 9
- segment is checked, to be sure that it holds the initial value of zero.
- It is important to prevent multiple installations, because installing saves
- the current interrupt handler's address. If NoReboot installed itself
- twice, it would save its own address on top of the original BIOS handler's
- saved address. And once that address is lost, it is impossible to restore
- it again later.
- Assuming it is safe to be installed, the next step is to ask DOS for the
- current interrupt handler's address using service 35h. This service
- expects the service number in AH, and the interrupt number in AL. To save
- a byte, both values are loaded at once. Service 35h returns the segment
- and address in ES:BX, and these are saved in the code segment. Because the
- original address will be called from within the interrupt handler, CS is
- the only register whose contents are known. Accessing data in DGROUP is
- more difficult, because an interrupt can occur at any time, and DS will
- likely not be holding the correct segment. [That is, execution could be
- at any point in the program when Ctrl-Alt-Del is pressed, including within
- a routine that has changed DS. So when NoReboot receives control it can't
- be certain that DS holds the segment for .Data variables it has defined.]
- Once the original interrupt handler address has been saved, NoReboot
- calls DOS again, but this time to assign the segment and address of its
- replacement handler in the interrupt vector table. It is easy to access
- the interrupt vector table directly using Mov instructions, but it is even
- easier to have DOS do that.
- Finally, NoReboot returns to the calling BASIC program, and all
- subsequent key presses are now routed to the NewInt9 procedure.
- NewInt9 must perform a few tricks, partly because it is handling a
- hardware interrupt. All interrupt handlers begin with the instruction Sti,
- which tells the 8088 to allow further interrupts to occur and be processed.
- Next, the two registers being used are saved on the stack, so they can be
- restored again later. Because a keyboard interrupt can occur at any time
- interrupting the process that is currently running, it is imperative that
- you not alter any aspect of the 8088's current state. This includes the
- settings of the Flags register as well. However, the Flags register is
- saved automatically by the 8088 as part of its handling of interrupts, so
- the flags don't have to be saved or restored manually using Pushf and Popf.
- The next sequence of instructions reads the key that was pressed from
- the keyboard's I/O port (60h), and compares that to the scan code for the
- Del key. If any other key was pressed, NoReboot jumps to the original
- keyboard handler in the ROM BIOS. Otherwise, it examines low memory to see
- if both the Ctrl and Alt keys are also currently pressed. Unless all three
- conditions are met, control passes on to the BIOS. But if Ctrl-Alt-Del is
- pressed, NoReboot handles the keystroke entirely on its own and ignores it.
- In that case DS and AX are restored, and NoReboot exits back to the
- underlying program.
- Notice the special form of return command, Iret (Interrupt Return).
- Like a conventional far return, Iret pops the address and segment to return
- to from the stack, but it also pops the Flags register that was stored
- there by the 8088 automatically.
- The final section of code restores the original interrupt vector, and
- clears the Old9Seg variable to zero. This lets NoReboot know that it is
- not installed, in case you call it again later.
- This same technique can be applied to handle other interrupt services,
- and I encourage you to experiment on your own. You could, for example,
- write a routine that takes over the communications interrupt, and displays
- a flashing box in a corner of the screen whenever characters are received.
- Likewise, you could modify this routine to create an on-screen display of
- the Caps Lock and Num Lock state. Each time one of those keys is pressed
- you would either print or clear a status message.
-
-
- DEBUGGING WITH CODEVIEW
- =======================
-
- As useful as CodeView can be for a purely BASIC program, it is even more
- necessary when writing in assembly language. CodeView lets you step
- through the code that BASIC generates to set up and call your subroutine,
- and then step through the routine a line at a time. Being able to watch
- your program as it executes helps you to quickly zero in on any problems.
- Further, CodeView shows you the current CPU register contents, as well as
- the value of memory locations about to be read from or written to.
- To debug an assembly language subroutine with CodeView, you must first
- assemble it using the /Zi option switch:
-
- masm routine /zi;
-
- Then you link the routine to your BASIC program using the /Co option. Of
- course, the BASIC program must also have been compiled using /Zi:
-
- bc program /o /zi;
- link program routine /co;
-
- Finally, you start CodeView specifying the name of the BASIC program:
-
- cv program
-
- Once the BASIC source code is showing on the screen you can step and trace
- through it as described in Chapter 4. As with BASIC subprograms and
- functions, to step into an assembler routine you press F8 at the CALL
- statement. If the routine is designed as a function you instead press F8
- at the line in which the function is referenced.
- Once CodeView has traced into the routine, you can press F3 to view the
- source code only, the assembly code only, or both intermixed. I usually
- prefer to view only my original source, but that hides the data memory
- addresses that MASM and LINK assigned. Usually you will not need to know
- those addresses, but there are times when this can be helpful. For
- example, when a program is not working correctly, the bug could be caused
- by a different portion of the program overwriting the named variables.
- Besides the F3 key, you can also use F4 and F7, and these have the same
- meaning as the same keys when used in the BASIC editor. Indeed, debugging
- an assembly language subroutine is quite similar to debugging a BASIC
- program as far as which keys are used.
-
-
- MASM 6.0 ENHANCEMENTS
- =====================
-
- All of the discussions in this chapter have focused on using MASM version
- 5.1. However, Microsoft's more recent version 6.0 introduces a number of
- significant changes and new features. Perhaps the most useful new feature
- in this release is the greatly improved documentation. The manuals that
- came with past versions of MASM were very dry, containing reams of facts
- but no practical advice or guidance. The new documentation include both
- facts and programming tips, and this addition is welcome indeed.
- If you already have existing assembly language source code, you may have
- to change it to accommodate the new MASM 6.0 conventions. In particular,
- MASM's handling of data structures has changed substantially, and in many
- cases code that used to work correctly no longer does. However, you can
- optionally use the /Zm command line switch, to tell MASM 6.0 to behave like
- the earlier 5.1 version.
- A new MASM.EXE program launcher is also included to offer a similar
- capability. Where older versions of MASM were named MASM.EXE, the new
- program is called ML.EXE. The MASM.EXE that now comes with MASM 6.0 simply
- passes the /Zm option on to ML, along with some other option switches that
- are needed to tell ML to mimic the older assembler's behavior.
-
-
- IMPROVED ASSEMBLY OPTIMIZATIONS
-
- Before MASM 6.0, a conditional jump was limited to a distance no greater
- than 128 bytes earlier or 127 bytes farther ahead in the code. When there
- was no way to restructure your code to accommodate this inherent 8088
- limitation, you had to use a conditional jump around another unconditional
- jump like this:
-
- ;if AX < 12 go to FarLabel
- Cmp AX,12 ;compare AX to 12
- Jnl NearLabel ;jump if not less over far jump
- Jmp FarLabel ;perform the far jump
- NearLabel:
- . ;program continues
- .
- . ;this label is more than
- FarLabel: ; 127 bytes past Jnl
-
- MASM 6.0 avoids this limitation and lets you use Jl to the far label
- directly, although it really just replaces your use of Jl with code
- equivalent to that shown above.
- Another, similar optimization affects unconditional jumps. As I
- mentioned earlier, each time MASM 5.1 encounters a label in your source
- code, it remembers its address in the resultant object code. Then if you
- jump backwards to that label later, MASM knows if it can use the shorter
- two-byte form of the Jmp instruction. But a forward jump to a near label
- requires you to explicitly state Jmp Short to obtain this code savings,
- since MASM 5.1 does not yet know the target label's address. Without
- Short, MASM 5.1 uses a long jump on a trial basis. If the jump turns out
- to be within the near range MASM goes back and patches the code to a short
- jump followed by a byte-wasting Nop (No Operation) instruction.
- MASM 6.0 avoids this problem by processing your source file in multiple
- passes. That is, MASM reads your code and assembles what it can, using far
- jumps when the target address has not yet been encountered. Then it
- processes that intermediate code again modifying its earlier output as
- appropriate. If a three-byte jump can be replaced with the two-byte
- version, MASM 6.0 rewrites the code sliding subsequent instructions back
- a byte. MASM 6.0 is called an *n-pass assembler*, because as many passes
- as needed are performed until the code is as small as possible.
-
-
- NEW SIMPLIFIED DIRECTIVES
-
- Besides the improved optimizing, MASM 6.0 offers several features borrowed
- from high-level languages. These include .IF, .ELSE, and .ELSEIF; .WHILE
- and .ENDW; and .REPEAT and .UNTIL. Unfortunately, these new constructs are
- modeled after the C language, and provide little if any clarification to
- BASIC programmers. For example, you can now write code such as this:
-
- .IF (AL < "0") || (AL > "9")
-
- which is equivalent to this BASIC statement:
-
- IF AL < ASC("0") OR AL > ASC("9")
-
- Even worse, the MASM manual does not document each directive showing
- precisely what it does to your code.
- Like C, BASIC's AND is replaced with a double ampersand (&&), testing
- for equality uses a double equals sign (==), and NOT is replaced with an
- exclamation point (!). Therefore, you could write assembly language source
- statements like these next two examples:
-
- .IF (AX != 14) && (BX < 10) ;IF AX <> 14 AND BX < 10 THEN
- Mov AX,SomeVar ;divide SomeVar by CX
- Cwd
- Div CX
- Mov SomeVar,AX
- .ENDIF
-
-
- .REPEAT
- Mov AH,1 ;ask for a keyboard character
- Int 21h ;through DOS
- .UNTIL (AL == 13) ;loop until they press Enter
-
- PROTO and INVOKE are two other new simplified directives, and it's hard
- for me to recommend using them for similar reasons. PROTO mimics C's
- function prototype capability, and lets you define a called procedure and
- its arguments. INVOKE then calls that routine passing the arguments you
- give it. To define a procedure called, say, MyProc, you would use PROTO
- like this:
-
- MyProc PROTO Var1:Word, Var2:Word, Var3:DWord
-
- Then to call MyProc you use INVOKE as follows:
-
- INVOKE MyProc, BX, 100, LongVar
-
- Thus, PROTO and INVOKE are very similar to DECLARE SUB and CALL in BASIC.
- The problem is that you have no way to know what code MASM generates for
- this command unless you create a sample program, assemble it, and examine
- the result using CodeView. In particular, how does the value 100 used here
- get onto the stack? As it turns out, assembling the preceding INVOKE
- command results in the following code:
-
- Push BX
- Mov AX,100
- Push AX
- Push Word Ptr [LongVar+2]
- Push Word Ptr [LongVar]
-
- As you can see, even if AX is holding an important value, its contents are
- destroyed when MASM assigns the value 100 prior to placing it on the stack.
- While I applaud Microsoft's attempts to make assembly language easier to
- use, such behavior can and will introduce subtle bugs. These bugs can be
- even harder to track down than usual, because you did not make the coding
- error, the assembler did! Since the whole point of programming in assembly
- language is to control fully what the CPU is doing, such hidden behavior
- can have disastrous effects.
- One new feature that I do find useful, however, is the ability to
- continue a line with a trailing comma. Often, a single source statement
- will extend into the comments column, spoiling the appearance of your
- listing. You can now avoid this by placing a comma in the middle of a
- logical line, and then continuing the remainder of the statement on the
- next line.
- Another very useful feature is MASM 6's ability to accept wild cards on
- the command line. For example, you can assemble all of the files in the
- current directory using the command masm *.asm;.
-
-
- TRICKS OF THE TRADE
- ===================
-
- The final topic I want to present is a variety of assembly language
- programming short cuts and other techniques I have developed over the
- years. In preceding sections you saw how Xor or Sub can be used to clear
- a register, using less code than Mov. And if you know that the high-byte
- portion of a register or memory variable is already zero, you can save a
- byte by assigning only the lower byte. And to clear both AX and DX you can
- use Xor with AX, and then Cwd to extend the zero into DX using only one
- additional byte. As you might imagine, there are many other ways to be
- clever in assembly language.
-
-
- MINIMIZE CODE TO ACCESS PARAMETERS
-
- When parameters are accessed within an assembly language subroutine, the
- usual way to get at them is through BP. Even when you use MASM's
- simplified directives, code to push BP, assign it from SP, and then
- reference the address on the stack is added to your program. In that case,
- the steps are simply hidden from you. Because BASIC (and indeed, every
- high-level language) requires you to preserve BP, one byte each is needed
- for the Push and Pop instructions.
- You can eliminate that overhead by taking advantage of the fact that the
- stack is always kept in DGROUP, and that SS and DS are equal. The trick
- is to use BX as a stack reference, because it doesn't need to be preserved.
- Unfortunately, this precludes using the simplified methods for parameter
- access. But when speed or code size are paramount or you have many
- routines, stack addressing via BX affords a real savings. Here's how you
- will design the routine, using an example that accesses an incoming string:
-
- GetString Proc ;one parameter, not shown
-
- Mov BX,SP ;address the stack manually using BX
- Mov BX,[BX+04] ;get the address for the string
- Mov CX,[BX] ;get the length of the string
- Jcxz Exit ;quit if the string is null
- Mov BX,[BX+02] ;get address of first character
-
- Exit:
- Retf 2 ;specify far return with 2 bytes
-
- GetString Endp
- End
-
- Because BP has not been pushed onto the stack, the incoming string
- descriptor address is at [BX+4] rather than [BX+6]. Other than that, the
- remainder of the routine proceeds as usual.
-
-
- BYTE SAVERS
-
- Another useful trick lets you save a byte when adding two to a variable.
- As you know, Inc and Dec when used with a register are always better than
- Add and Sub, because they are one-byte instructions. Therefore, two Inc
- or Dec commands in a row are still better than Add AX,2 which requires
- three bytes. However, you must never do this with SP. The stack pointer
- must always hold an even number, and it is possible that an interrupt could
- come along after the first Inc or Dec, but before the second has executed.
- Which brings up a related byte saver.
- If you need only a single word of local stack storage, don't use Sub
- SP,2 to allocate the space and Add SP,2 later to clear it. Instead, simply
- use Push AX, or Push with any other register. Likewise, just before
- returning to BASIC, pop any register that doesn't return information, such
- as CX or BX.
-
-
- Rep Always Clears CX
-
- Another trick you can take advantage of is that CX is often zero after a
- repeating string command that uses Rep. Zero is a common value in assembly
- language programming, and you can usually save a byte by using a register
- instead of a constant zero. In particular, if you are copying a file name
- to a buffer and adding a CHR$(0) to the end, you can use code like this:
- .
- . ;set up DS:SI and ES:DI here
- Mov CX,NumBytes
- Rep Movsb
- Mov [DI],CL ;tack a zero byte onto the end
- .
- .
-
- This trick is made even more valuable by the fact that DI is left pointing
- at the byte just past the data that was just copied. Of course, CX is not
- necessarily zero after Repe or Repne, because those forms of Rep can
- terminate before CX is exhausted.
-
-
- Use AX Where Possible
-
- Another little-known fact is that memory operations that use AX are one
- byte smaller than equivalent operations on any other register. That is,
- Mov BX,KeyCode results in four bytes of code, whereas Mov AX,KeyCode
- creates only three. I often use the DOS DEBUG program for quick tests,
- just to see which sequence of instructions results in less code. Since
- DEBUG does not let you specify a variable name, use [100] or any other
- address instead:
-
- -a 100
- -####:0100 Mov AX,[100]
- -####:0103 Mov BX,[100]
- -####:0107 <press Enter to stop assembling>
- -u 100,106
- ####:0100 A10001 MOV AX,[0100]
- ####:0103 8B1E0001 MOV BX,[0100]
- -q
-
- This sample session tells DEBUG to begin assembling at address 100 (the
- default for .COM files), and then assemble the two instructions shown.
- When you are done press Enter at the dash prompt, and then unassemble the
- results and quit. As you can see, using AX creates one less byte of code.
-
-
- Multiplying and Dividing By a Power of 2
-
- Because of the way binary numbers are organized, shifting the bits left
- or right can provide a very fast way to multiply or divide by a power of
- two. And because the bit shifting commands can be used with all but the
- segment registers, this can also save you from having to copy the data to
- AX or DX:AX first. To divide a register by two simply shift the bits right
- one position:
-
- Shr CX,1
-
- And to multiply by two shift them left:
-
- Shl SI,1
-
- If you need to multiply or divide by four, eight, sixteen, and so forth,
- the shift count must first be placed into the CL register:
-
- Mov CL,5 ;prepare to divide BP by 32
- Shr BP,CL
-
- On 80186 and later processors you can specify a shift count directly.
- Unfortunately, this doesn't work with an 8088, so CL must be used. Still,
- multiplying and dividing are extremely slow instructions on an 8088, so the
- added setup will be more than offset if speed is the primary factor.
-
-
- Low Memory is at Segment Zero
-
- Another useful byte saver is to treat the BIOS data area in low memory as
- being at segment zero, instead of the more commonly used segment 40h. By
- convention, the BIOS data area is said to reside at segment 40h, even
- though a number of segment/address pairs can be used to access that data.
- I mentioned this briefly in Chapter 11, in the discussions about using
- BASIC's CALL Interrupt. Since Xor or Sub can be used to clear a register
- to zero with one byte less code than assigning it a value of 40, I use this
- technique frequently:
-
- This example generates 9 bytes:
- Xor AX,AX
- Mov DS,AX
- Test Byte Ptr [417h],8 ;see if the Alt key is depressed
-
- And this example creates 10 bytes:
- Mov AX,40h
- Mov DS,AX
- Test Byte Ptr [17h],8
-
-
- Scanning An ASCIIZ String
-
- Because ASCIIZ strings are used in programs that access DOS services,
- searching those strings to find the end is a common operation. For
- example, the GetNames function does this to determine the length of each
- file name before assigning it to elements in the incoming string array.
- In that routine CX is assigned to 13, which is the maximum length a file
- name can be. Since CX is decremented for each character that is examined,
- the length is calculated by subtracting CX from 13, which requires an extra
- register.
- As long as you are certain that a zero byte is present, you can use a
- clever trick to determine directly the number of bytes that were searched.
- Instead of loading CX with the maximum number of bytes to scan, assign it
- to -1. As each character is searched CX is decremented, which results in
- a negative version of the number of bytes. Then the NOT instruction can
- be used to revert that to a positive number:
-
- Mov ES,Segment ;point ES:DI to the start of the data
- Mov DI,Address
- Cld ;ensure that scanning is forward
- Mov CX,-1 ;set CX to -1
- Mov AL,0 ;search for a zero byte
-
- Repne Scasb ;scan the string
- Not CX ;convert to a positive number
- Dec CX ;don't include the zero byte itself
- Mov AX,CX ;now AX holds the length of the string
-
- As you learned in Chapter 2, BASIC's NOT instruction flips all of the bits,
- converting ones to zeros and vice versa. The assembly language version
- works the same way, and can be used with registers or memory locations.
-
-
- CYCLE SAVERS
-
- Besides savings bytes when possible, most assembly language programmers
- also like to save clock cycles. Every assembler instruction requires a
- certain amount of CPU timing cycles to execute, although there are other
- factors that also affect the actual throughput of a given piece of code.
- But instructions with the fewest number of clock cycles as published by
- Intel are always faster than those that require more cycles.
-
-
- Move and Store Words Instead of Bytes
-
- One very effective speed enhancement is to copy and store words when
- possible, instead of bytes. On 80286 and later processors, words are moved
- and stored as quickly as bytes. Therefore, moving 50 words is much faster
- than moving 100 bytes. If you know ahead of time how many bytes are going
- to be processed and that the number is even, you can simply load CX with
- half the value, and use Rep Movsw or Rep Stosw instead of Rep Movsb or Rep
- Stosb. [This trick can be used even if the program runs on an 8088, but
- the speedup only occurs with 80286 and later CPUs.] With only a little
- added code you can also use this technique to determine at runtime if an
- odd byte needs to be processed. Here's one way to do that:
-
- Shr CX,1 ;divide CX by 2
- Rep Movsw ;copy the words
- Jnc Done ;the Carry Flag is clear
- Movsb ;copy the odd byte
- Done:
- . ;program continues
- .
-
- First, CX is divided by 2, and the odd bit, if there was one, is stored
- by the CPU in the Carry Flag. Then the data words are copied to their
- destination. Finally, the Carry flag is tested and the program either
- copies a single additional byte or skips over that command.
-
-
- A Jump Not Taken is Faster Than One That is
-
- And this brings us to yet another cycle saver. In some cases the Jnc will
- be executed, and in others it will not. And in most programs, the chances
- of either happening are about fifty-fifty. But if you know ahead of time
- that a particular action will happen less often than another, you can take
- advantage of another 8088 fact: A jump not taken is always faster than one
- that is taken.
- Each time the 8088 jumps to a new location or calls a procedure, it
- discards its *pre-fetch queue*. The pre-fetch queue is a small area of
- memory on the CPU itself that holds the next few instructions to be
- executed. In many cases, the 8088 can do several things at once. So while
- it is adding or subtracting numbers, it simultaneously fetches instruction
- bytes from your code, in anticipation of what it will do next. This lets
- the CPU act on the subsequent instructions very quickly, because they are
- already in its own local on-chip memory. Just as data in registers can be
- accessed faster than data that must be read from memory, so too can
- instructions that are already in the CPU.
- But when execution branches to a new location, any bytes present in the
- pre-fetch queue are obsolete. Therefore, the 8088 must read the new bytes
- at the new location, which takes additional time. If you have a routine
- that makes a test repeatedly within a loop you should change the logic as
- necessary, to branch on the less likely situation. That is, instead of Jne
- you might use Je, or vice versa.
-
-
- MISCELLANEOUS TECHNIQUES
-
- One very powerful technique you will surely find useful is self-modifying
- code. As its name implies, self-modifying code actually writes new
- instructions into its own code segment, and this is useful in a variety
- of situations. For example, if you are writing a routine that accepts a
- variable number of parameters this lets you patch the Ret instruction to
- be Ret 2, Ret 4, and so forth.
- One warning, however, is related to the pre-fetch queue. If a byte or
- word has already been read into the CPU, changing it in the code segment
- has no effect. Worse, there is no way to know for certain which bytes will
- have already been read, because the size of the pre-fetch queue has grown
- with each new CPU from Intel. For example, only four bytes are allocated
- for a pre-fetch queue on an 8088, but the 80386 uses 16 bytes.
- In general, if the code you are patching is located at least a few dozen
- bytes farther in the program, you should be safe. Such self-modifying code
- was used in the SORT.ASM routine shown in Chapter 8, to let the same code
- sort either forward or backward. There, the bytes that represent Jae and
- Jbe were assigned to AL and AH, and the code was patched based on the
- incoming sort direction. Since the patching takes place a hundred or so
- bytes earlier in the program, it is unlikely that this routine will fail
- with future processors.
-
-
- Static-Free CGA Text Display
-
- The final technique you will find useful is writing to CGA text mode video
- memory without creating a disturbance. When IBM designed the original CGA
- adapter they skimped on the design, using circuitry that shares a single
- address line for both the 8088 CPU and the video hardware that updates the
- screen. Even when a program is not reading from or writing to display
- memory, that memory is still read periodically by the display adapter and
- sent to the monitor. Therefore, accessing that memory directly from an
- assembly language routine creates a disturbing burst of static that is
- visible on the monitor. This is caused by the conflict of the CPU and the
- video adapter accessing the same video memory addresses at the same time.
- Newer CGA adapters employ a dual-port design that arbitrates
- simultaneous read and write requests, thereby eliminating this problem.
- And, of course, EGA and VGA adapters are much more sophisticated than the
- CGA, and fortunately also more common these days. However, you can avoid
- the screen disturbance on older CGA adapters by synchronizing your reading
- and writing with the horizontal retrace timing.
- As you undoubtedly know, the image on a CRT is drawn by scanning a
- single dot horizontally across each successive row. This happens so
- quickly that the eye perceives the moving dot as an entire image. After
- each row is drawn, the dot is turned off, quickly placed at the start of
- the next row below, and then turned on again. By writing to the screen
- only while the dot is turned off you can hide the memory conflicts that
- cause static.
- The short code fragment below shows how to synchronize video writing
- with the CGA's horizontal retrace. In a windowing routine that also needs
- to read video memory, you would use the same technique just before each
- byte or word is read.
-
- .
- .
- Mov SI,Descriptor ;get the incoming descriptor address
- Mov CX,[SI] ;the string's length goes in CX
- Mov SI,[SI+2] ;and the address of the data in SI
-
- Mov AX,&HB800 ;load ES with the CGA video segment
- Mov ES,AX ;through AX
- Xor DI,DI ;point DI to the upper left corner
-
- Mov AH,Color ;load color parameter (passed BYVAL)
- Jcxz Done ;don't try to print a null string!
-
- No_Retrace:
- In AL,DX ;get the video status byte
- Test AL,1 ;test the horizontal retrace bit
- Jnz No_Retrace ;if doing retrace, wait until done
- Cli ;disable interrupts until we're done
-
- Retrace:
- In AL,DX ;get the status byte again
- Test AL,1 ;are we currently doing a retrace?
- Jz Retrace ;no, wait until we are
- Lodsb ;load the current character
- Stosw ;store the character and attribute
-
- Sti ;re-enable interrupts
- Loop No_Retrace ;loop until the string is printed
-
- Done:
- . ;program continues or exits here
- .
-
- The current horizontal retrace status can be read using the In instruction,
- and then masking off all but the lowest bit. To protect against the case
- where the print loop is entered just as the retrace is about to end, this
- routine waits until a new period has just begun. This is not unlike the
- empty loop used in the benchmark examples in Chapter 9, that waited for a
- new system clock cycle to begin.
-
-
- SUMMARY
- =======
-
- In this final chapter you have learned what assembly language programming
- is all about, and how it can help you as a BASIC programmer. There is no
- doubt that using assembly language is more tedious than BASIC, but the
- overall methods and code structures are similar.
- You learned about the 8088's registers, and why operations that use them
- are faster than similar operations on memory variables. The string
- instructions are particularly useful, because they are very small and do
- several things at once. Coupled with the Rep prefix these commands can
- replace many separate Mov and Inc and Cmp statements. You also learned how
- to perform simple calculations in assembly language, and an example showed
- how to translate simple BASIC integer and floating point expressions.
- This chapter explained how the stack operates, and how procedures are
- designed to accept passed parameters. The new simplified directives
- introduced with MASM 5.1 eliminate the need to define segments and figure
- parameter stack displacements in your routines. This chapter also
- explained how to call DOS and BIOS interrupts from assembly language.
- You learned how to access every kind of data a BASIC program can pass
- to a routine, including near and far strings, integers, and even floating
- point values. The section that described arrays showed how to access both
- near and far data, and even huge arrays that span multiple segments.
- Besides conventional called procedures, you also learned how to create
- functions that can return any type of data. Several innovative techniques
- were presented, including a method for creating a single procedure that can
- work with both near and far strings, and even with different versions of
- the BASIC compiler. Equally innovative are the methods that show how to
- write floating point instructions and tie them into BASIC's software
- emulator. And if you are not certain how to code a particular floating
- point instruction, you can create a short BASIC program and then examine
- its code using CodeView.
- This chapter explained many of MASM's features, such as initialized
- data, conditional assembly, and defining structures and macros. In
- particular, macros can greatly simplify coding redundant instructions and
- data definitions. Furthermore, MASM can calculate data addresses and
- lengths automatically, reducing your work when the data must be changed
- later on.
- Because so many different data items all compete for the same 64K near
- memory segment, it is often desireable to store working variables on the
- system stack. Likewise, when large amounts of data are involved, variables
- and tables can be stored in the code segment. Both of these techniques
- were described in depth, and accompanying examples showed how to do this
- in context.
- Several of BASIC's most useful internal variables and procedures were
- described, showing their public names and parameter requirements. The
- GetNames function brought all of this information together, showing how to
- read an array descriptor, redimension a string array, and assign individual
- elements--all using code that works identically with both near and far
- strings.
- You also learned how to write an interrupt handler that can be installed
- and deinstalled from within a BASIC program. The example showed how to
- take over the keyboard interrupt; however, the same technique can be
- applied to nearly any other hardware or software interrupt as well.
- Finally, this chapter described many useful tricks and techniques that
- help to reduce the size of your assembly language routines, and also make
- them faster. Many operations that use the AX register result in less code
- than the same operations using other registers. And when moving or storing
- contiguous data, accessing the data as words instead of bytes can sometimes
- yield a nearly two-fold speed improvement. When in doubt about which of
- several sequences of code is smaller, you can use the DOS DEBUG utility to
- quickly determine that.